如果在使用VS2012 RC时main()退出后调用,则std :: thread :: join()会挂起

时间:2012-06-06 13:30:23

标签: c++ visual-c++ c++11 stdthread visual-c++-2012

如果在Ubuntu 12.04上使用Clang 3.2或GCC 4.7进行编译,则以下示例成功运行(即不挂起),但如果我使用VS11 Beta或VS2012 RC进行编译,则会挂起。

#include <iostream>
#include <string>
#include <thread>
#include "boost/thread/thread.hpp"

void SleepFor(int ms) {
  std::this_thread::sleep_for(std::chrono::milliseconds(ms));
}

template<typename T>
class ThreadTest {
 public:
  ThreadTest() : thread_([] { SleepFor(10); }) {}
  ~ThreadTest() {
    std::cout << "About to join\t" << id() << '\n';
    thread_.join();
    std::cout << "Joined\t\t" << id() << '\n';
  }
 private:
  std::string id() const { return typeid(decltype(thread_)).name(); }
  T thread_;
};

int main() {
  static ThreadTest<std::thread> std_test;
  static ThreadTest<boost::thread> boost_test;
//  SleepFor(100);
}

问题似乎是std::thread::join()main退出后调用时永远不会返回。它在cthread.c中定义的WaitForSingleObject _Thrd_join处被阻止。

SleepFor(100);末尾取消注释main可以让程序正常退出,同时使std_test非静态。使用boost::thread也可以避免此问题。

所以我想知道我是否在这里调用未定义的行为(我似乎不太可能),或者我是否应该提交针对VS2012的错误?

5 个答案:

答案 0 :(得分:25)

使用VS2012 RTM在他的连接错误(https://connect.microsoft.com/VisualStudio/feedback/details/747145)中跟踪Fraser的示例代码似乎显示了一个相当简单的死锁情况。这可能不是std::thread特有的 - 可能_beginthreadex遭遇同样的命运。

我在调试器中看到的内容如下:

在主线程上,main()函数已经完成,进程清理代码已经获得了一个名为_EXIT_LOCK1的关键部分,称为ThreadTest的析构函数,并且正在等待(无限期)在第二个线程上退出(通过调用join())。

第二个线程的匿名函数已完成,并且在线程清理代码中等待获取_EXIT_LOCK1临界区。不幸的是,由于事物的时间安排(第二个线程的匿名函数的生命周期超过main()函数的生命周期),主线程已经拥有该关键部分。

<强> DEADLOCK。

延长main()生命周期的任何内容,以便第二个线程可以在主线程避免死锁情况之前获取_EXIT_LOCK1。这就是为什么取消注释main()中的睡眠会导致干净关闭。

或者,如果从ThreadTest局部变量中删除static关键字,则析构函数调用将移动到main()函数的末尾(而不是在进程清理代码中),然后阻塞直到第二个线程已退出 - 避免死锁情况。

或者您可以向调用ThreadTest的{​​{1}}添加一个函数,并在join()结束时调用该函数 - 再次避免死锁情况。

答案 1 :(得分:6)

我意识到这是一个关于VS2012的老问题,但该错误仍然存​​在于VS2013中。对于那些卡在VS2013上的人,可能是由于微软拒绝为VS2015提供升级价格,我提供了以下分析和解决方法。

问题是at_thread_exit_mutex使用的互斥锁(_Cnd_do_broadcast_at_thread_exit())尚未初始化,或者已经被破坏,具体取决于具体情况。在前一种情况下,_Cnd_do_broadcast_at_thread_exit()尝试在关闭期间初始化互斥锁,从而导致死锁。在后一种情况下,互斥体已经通过atexit堆栈销毁,程序将在出路时崩溃。

我找到的解决方案是在程序启动期间尽早明确地调用_Cnd_do_broadcast_at_thread_exit()(值得庆幸的是公开声明)。这样可以在其他人尝试访问互斥锁之前创建互斥锁,并确保互斥锁继续存在直到最后一刻。

因此,要解决此问题,请在源模块的底部插入以下代码,例如main()下面的某个位置。

#pragma warning(disable:4073) // initializers put in library initialization area
#pragma init_seg(lib)

#if _MSC_VER < 1900
struct VS2013_threading_fix
{
    VS2013_threading_fix()
    {
        _Cnd_do_broadcast_at_thread_exit();
    }
} threading_fix;
#endif

答案 2 :(得分:1)

我相信你的线程已经被终止,并且在你的main函数终止之后和静态破坏之前释放了它们的资源。这是VC运行时至少可以追溯到VC6的行为。

Do child threads exit when the parent thread terminates

boost thread and process cleanup on windows

答案 3 :(得分:0)

我的回答为时已晚,但希望能帮助到某人。

我被这个错误困住了,我找到了解决这个问题的技巧,它在我的代码中起作用。

int main()
{
    ThreadTest trick_obj;  //trick... You can put this line of code anywhere
    static ThreadTest std_test;
    return 1;
}

答案 4 :(得分:-1)

我一直在与这个bug争斗一天,并找到了以下解决方法,结果证明这是最不脏的技巧:

可以使用标准Windows API函数调用ExitThread()来终止线程,而不是返回。这个方法当然可能搞乱了std :: thread对象和相关库的内部状态,但是因为程序无论如何都会终止,所以就这样吧。

#include <windows.h>

template<typename T>
class ThreadTest {
 public:
  ThreadTest() : thread_([] { SleepFor(10); ExitThread(NULL); }) {}
  ~ThreadTest() {
    std::cout << "About to join\t" << id() << '\n';
    thread_.join();
    std::cout << "Joined\t\t" << id() << '\n';
  }
 private:
  std::string id() const { return typeid(decltype(thread_)).name(); }
  T thread_;
};

join()调用显然可以正常工作。但是,我选择在我们的解决方案中使用更安全的方法。可以通过std :: thread :: native_handle()获取线程HANDLE。使用此句柄,我们可以直接调用Windows API来加入线程:

WaitForSingleObject(thread_.native_handle(), INFINITE);
CloseHandle(thread_.native_handle());

此后,不得销毁std :: thread对象,因为析构函数会尝试第二次加入该线程。所以我们只是将std :: thread对象悬挂在程序出口处。