【问题标题】:Terminate called in move assignment of std::thread在 std::thread 的移动分配中调用终止
【发布时间】:2021-05-15 04:38:28
【问题描述】:

我有一个由多个用户使用的多线程应用程序。对于某些用户,运行应用程序会导致

terminate called without an active exception
Aborted

使用 GDB 运行应用程序会产生以下输出:

Thread 1 ... received signal SIGABRT, Aborted.
__GI__raise (sig=sig@entry=6) at ../sysdeps/unix/sysv/linux/raise.c:51
51       ../sysdeps/unix/sysv/linux/raise.c: No such file or directory.
(gdb) where
#0 _GI_raise (sig=sig@entry=6) at ./sysdeps/unix/sysv/linux/raise.c:51
#1 0x00007f46925e9921 in GI_abort () at abort.c:79
#2 0x0000744692404957 in ?? (from /usr/lib/x86_64-linux-gnu/libstdc++.50.6
#3 0x00007F4692fe2ae6 in ?? (from /usr/lib/x86_64-linux-gnu/libstdc++.50.6
#4 0x00007F4692fe2b21 in std::terminate() ()
  from /usr/lib/x86_64-linux-gnu/libstdc++.50.6
#5 0X000056407cb17783 in std::thread::operator=(std::thread&&) ()
...

在线查看,错误似乎是由于处理线程清理不当造成的(其中一个线程仍可连接)。下面的代码是在应用程序中找到的代码示例。

看门狗

class WatchDog {
  std::thread t_;
  std::atomic<bool> run_;

public:
  WatchDog(){};

  void Init() { t_ = std::thread(&WatchDog::Log, this); }

  void Log() {

    run_ = true;
    while (run_) {
      std::cout << "Operational" << std::endl;
      std::this_thread::sleep_for(std::chrono::milliseconds(200));
      // throw;
    }
  }

  void Stop() { run_ = false; }

  ~WatchDog() {
    if (t_.joinable())
      t_.join();
  }
};

主要

int main() {
  WatchDog dog;
  dog.Init();

  std::this_thread::sleep_for(std::chrono::seconds(1));
  dog.Stop();
}

示例剥离的应用程序运行无故障,并且在实际应用程序中也遵循了 RAII 习惯用法。不过,仔细回顾 GDB 结果,似乎终止调用是在移动赋值构造函数本身中对t_ 本身进行的。关于如何发生这种情况的任何解释以及调试它的建议?感谢您的帮助。


编辑

谢谢,Slava、BitTickler、cdhowie.. 我不知道 std::optional。我注意到我在其他几个地方犯了设计错误,所以想创建一个 ThreadWrapper 类。归功于https://thispointer.com/c11-how-to-use-stdthread-as-a-member-variable-in-class/。通过将this 传递给std::thread 的能力对其进行了扩展,因为我确实需要访问 WatchDog 类。

class ThreadWrapper {
public:
  // Delete copy constructor
  ThreadWrapper(const ThreadWrapper &) = delete;

  // Delete assignment constructor
  ThreadWrapper &operator=(const ThreadWrapper &) = delete;

  // Parameterized Constructor
  template <class F, class... Args>
  explicit ThreadWrapper(F&& func, Args &&... args)
      : thread_(std::forward<F>(func), std::forward<Args>(args)...) {}

  // Move constructor
  ThreadWrapper(ThreadWrapper &&obj) : thread_(std::move(obj.thread_)) {}

  // Move Assignment Constructor
  ThreadWrapper &operator=(ThreadWrapper &&obj) {
    if (thread_.joinable()) {
      thread_.join();
    }
    thread_ = std::move(obj.thread_);
    return *this;
  }

  ~ThreadWrapper() {
    if (thread_.joinable()) {
      thread_.join();
    }
  }

private:
  std::thread thread_;
};

WatchDog 类中保存的 ThreadWrapper 对象现在是否可以安全地避免对 Init 的两次潜在调用?计划在 WatchDog 构造函数中初始化 threadwrapper_,但据我所知更多。再次感谢大家。

threadwrapper_ = ThreadWrapper(&WatchDog::Log, this);

【问题讨论】:

  • 此示例代码是否在用户计算机上出现相同的故障?如果不是,则删除的代码可能是相关的。
  • 我们无法真正帮助您修复有效的代码。我们可以得到一个真正重现问题的minimal reproducible example 吗?
  • 如果你像这样重写void Init() { t_ = std::thread(&amp;WatchDog::Log, this); } 会发生什么:void Init() { t_ = std::move(std::thread(&amp;WatchDog::Log, this)); }
  • @BitTickler 功能相同
  • 禁用 WatchDog 类的移动构造函数和移动赋值运算符,看看是否出现编译错误。很可能在代码中的某处分配给 WatchDog 类本身

标签: c++ multithreading terminate


【解决方案1】:

std::thread::operator=

如果*this 仍有关联的运行线程(即joinable() == true),请调用std::terminate()

看起来您的 t_ 有一个关联的正在运行的线程,但您的非真实代码没有显示这一点。

【讨论】:

  • 谢谢。这对我来说很有意义。让我看看我是否可以尝试找出为什么我会在代码中调用 Init 两次。谢谢!
【解决方案2】:

要建立在现有答案的基础上,最有可能发生的是您在同一个对象上调用 Init() 两次,这会导致分配给现有(可连接)线程,这是不允许的。考虑使用新界面重新设计此类。

  • Init() 应该隐式发生在构造中。
  • Stop() 应该在销毁时隐式发生。
  • Log() 设为私有且 const 正确。

使用此实现,不可能意外调用Init() 两次,并且在销毁时会自动进行清理。 (在您的实现中,如果您忘记调用 Stop(),那么线程将永远不会加入,因为 run_ 永远不会设置为 false。)

如果您希望能够拥有“可能处于活动状态的WatchDog”,那么您可以简单地使用std::optional&lt;WatchDog&gt;

class WatchDog {
  std::atomic<bool> run_;
  std::thread t_;

public:
  WatchDog();
  ~WatchDog();

private:
  void Log() const;
};

WatchDog::WatchDog() :
  run_{true},
  t_{&WatchDog::Log, this} {}

void WatchDog::Log() const {
  while (run_) {
    std::cout << "Operational" << std::endl;
    std::this_thread::sleep_for(std::chrono::milliseconds(200));
    // throw;
  }
}

WatchDog::~WatchDog() {
  run_ = false;
  if (t_.joinable()) {
    t_.join();
  }
}

通过此实现,您给定的main() 变为:

int main() {
    WatchDog dog;
    std::this_thread::sleep_for(std::chrono::seconds(2));
}

如果您想在更动态的设置中更明确地控制对象的生命周期,这就是std::optional 的用武之地:

int main() {
    std::optional<WatchDog> dog;
    dog.emplace(); // Replaces Init()
    std::this_thread::sleep_for(std::chrono::seconds(2));
    dog.reset(); // Replaces Stop()
}

显然,这将具有与另一个 main() 示例相同的可观察行为,但重点是说明如果对象的生命周期需要更复杂且不受价值。

这解决了您遇到的Init() 被调用两次的问题,因为std::optional::emplace() 将在创建新值之前破坏包含的值。当然,如果您只想确保有一个活动的WatchDog(而不是不必要地破坏和创建一个),那么您可以执行if (!dog) { dog.emplace(); } 之类的操作。

附带说明,如果WatchDog::Log 从未使用this,则可以将其设为static,然后特定线程不会绑定到特定WatchDog 实例。

【讨论】:

  • 在您的代码中,WatchDog::Log 可能会在初始化之前使用run_
  • 感谢您的建议。我将按照建议重新设计它。
  • 对象实例的 2 阶段初始化是一个常见的用例,例如在已关闭异常的嵌入式编程中。
  • @Slava 确实,不知道我是怎么错过的——修复了。
  • @BitTickler 当然。这就是std::optional 可以派上用场的地方。
猜你喜欢
  • 1970-01-01
  • 2020-03-06
  • 2013-07-14
  • 2021-04-02
  • 1970-01-01
  • 2014-10-14
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多