微信公众号搜"智元新知"关注
微信扫一扫可直接关注哦!

在 std::thread 的移动分配中调用终止

如何解决在 std::thread 的移动分配中调用终止

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

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);

解决方法

std::thread::operator=

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

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

,

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

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

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

如果您想要拥有“可能处于活动状态的 WatchDog”,那么您可以简单地使用 std::optional<WatchDog>

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 实例。

版权声明:本文内容由互联网用户自发贡献,该文观点与技术仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 dio@foxmail.com 举报,一经查实,本站将立刻删除。