25

如果在 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.cWaitForSingleObject中定义的地方被阻塞。_Thrd_join

SleepFor(100);在末尾取消注释main可以让程序正确退出,就像制作std_test非静态一样。使用boost::thread也避免了这个问题。

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

4

5 回答 5

26

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

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

在主线程上,该main()函数已完成,进程清理代码已获得一个称为 的临界区_EXIT_LOCK1,称为 的析构函数ThreadTest,并且正在(无限期地)等待第二个线程退出(通过对 的调用join())。

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

僵局。

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

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

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

于 2012-11-22T07:37:15.143 回答
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
于 2015-12-10T16:46:26.383 回答
1

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

父线程终止时子线程是否退出

在 Windows 上提升线程和进程清理

于 2012-06-06T13:38:49.590 回答
0

我的回答为时已晚,但希望对某人有所帮助。

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

int main()
{
    ThreadTest trick_obj;  //trick... You can put this line of code anywhere
    static ThreadTest std_test;
    return 1;
}
于 2019-11-09T08:40:34.463 回答
-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 对象在程序退出时悬空。

于 2013-11-03T22:17:54.517 回答