如果在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的错误?
答案 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的行为。
答案 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对象悬挂在程序出口处。