【问题标题】:std::thread::join() hangs if called after main() exits when using VS2012 RC使用 VS2012 RC 时,如果在 main() 退出后调用 std::thread::join() 将挂起
【发布时间】:2012-06-10 12:41:36
【问题描述】:

如果在 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);
}

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

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

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

【问题讨论】:

  • 测试版和候选发布版问题?
  • @Jaywalker 好吧,我想这就是我要问的。如果在main 退出后调用join(),它不会挂起是否合理。如果是这样,这是一个我希望尽快进入 MS Connect 的错误。
  • 你能不能测试一下std::thread::joinable()在挂起之前是否仍然返回true?
  • 请在Microsoft Connect 上提交一个错误(我不知道这是否实际上 一个错误;我对std::thread 规范不太熟悉。但是尽管如此,还是值得报告的。)
  • @Plexico thread_.joinable() 确实返回 true

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


【解决方案1】:

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

Do child threads exit when the parent thread terminates

boost thread and process cleanup on windows

【讨论】:

  • 这个答案是针对windows的,但我假设你使用的是VS。
  • 我使用的是 Windows,但很遗憾,我不确定您的回答是否正确。主线程是正在执行join 的线程,所以即使函数main 已经退出,主线程并没有终止。还是谢谢。
  • 也许杀死线程的代码位于运行时主调用之外的某个位置,但我可以向您保证,在调用析构函数时,您的线程已经消失。
  • 我可以在程序挂起时暂停执行,并查看试图完成 join 调用的主线程和 mlock.c 的 _lock 中的子线程的堆栈,所以线程尚未终止。另外,您的回答并没有真正回答我的问题。这是由于未定义行为的结果还是 VS2012 中的错误?
【解决方案2】:

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

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

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

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

死锁。

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

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

或者您可以向ThreadTest 添加一个调用join() 的函数,并在main() 的末尾调用该函数——再次避免死锁情况。

【讨论】:

  • @CWoods - 您可以将我的 MS Connect 错误报告的链接粘贴到您的答案中吗?我想将你的标记为正确并删除我的。
  • @Frazer - 完成!很高兴能帮上忙!
  • 难以置信这个问题依然存在并且影响到C++11线程。 10 年前,我在 Windows 端口的 pthreads 上看到过类似的行为......
  • 是的,_beginthreadex() 的错误仍然存​​在于 MSVC++2013 中:我刚刚从 C++11 thread 重构为 MSVC++ _beginthreadex(),没办法。 MSDN还说CreateThread不能用于使用CRT的线程(我需要FILE*操作):msdn.microsoft.com/en-us/library/windows/desktop/…。也没有选项可以重构为非静态变量,因为线程发生在日志单例中。所以 MSVC++ 2013 在线程上被破坏了,CUDA Toolkit 还不支持 MSVC++2015。 %(
  • @CWoods MS 链接错误报告已损坏。你有更新的链接吗?
【解决方案3】:

我与这个 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 对象在程序退出时悬空。

【讨论】:

    【解决方案4】:

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

    问题是_Cnd_do_broadcast_at_thread_exit() 使用的互斥锁(at_thread_exit_mutex)要么尚未初始化,要么已经被销毁,具体取决于具体情况。在前一种情况下,_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
    

    【讨论】:

    • 此解决方法在 Visual Studio 2013 Ultimate 中对我不起作用。
    • 对于 DLL 中的单例活动对象,MSVC++2013 Update 5 对我也不起作用。
    【解决方案5】:

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

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

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

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2021-04-02
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2023-03-10
      • 2014-03-06
      • 1970-01-01
      相关资源
      最近更新 更多