【问题标题】:std::thread cause deadlock in DLLMainstd::thread 导致 DLLMain 死锁
【发布时间】:2015-11-21 23:58:57
【问题描述】:

所以,这就是我要说的:std 很复杂。

在 VS2013 中这个简单的程序会导致死锁。

#include <thread>
#include <windows.h>

void foo()
{
}

void initialize()
{
    std::thread t(foo);
}

BOOL APIENTRY DllMain(HMODULE, DWORD reason, LPVOID)
{
    switch (reason)
    {
    case DLL_PROCESS_ATTACH:
        initialize();
        break;
    case DLL_THREAD_ATTACH:
        break;
    case DLL_THREAD_DETACH:
        break;
    case DLL_PROCESS_DETACH:
        break;
    }
    return TRUE;
}

在 DLLMain 中创建线程是完全错误的吗?这不是真的。从 Microsoft 的文档“创建 DLL 的最佳实践”: "如果您不与其他线程同步,创建线程可以工作 线程”。所以 CreateThread 有效,_beginthreadex 有效,并且 boost::thread 有效,但 std::thread 无效。这是 调用栈:

ntdll.dll!_NtWaitForSingleObject@12()
KernelBase.dll!_WaitForSingleObjectEx@12()
msvcr120d.dll!Concurrency::details::ExternalContextBase::Block() Line 151
msvcr120d.dll!Concurrency::Context::Block() Line 63
msvcr120d.dll!Concurrency::details::_Condition_variable::wait(Concurrency::critical_section & _Lck) Line 595
msvcp120d.dll!do_wait(_Cnd_internal_imp_t * * cond, _Mtx_internal_imp_t * * mtx, const xtime * target) Line 54
msvcp120d.dll!_Cnd_wait(_Cnd_internal_imp_t * * cond, _Mtx_internal_imp_t * * mtx) Line 81
msvcp120d.dll!std::_Cnd_waitX(_Cnd_internal_imp_t * * _Cnd, _Mtx_internal_imp_t * * _Mtx) Line 93
msvcp120d.dll!std::_Pad::_Launch(_Thrd_imp_t * _Thr) Line 73
mod.dll!std::_Launch<std::_Bind<1,void,void (__cdecl*const)(void)> >(_Thrd_imp_t * _Thr, std::_Bind<1,void,void (__cdecl*const)(void)> && _Tg) Line 206
mod.dll!std::thread::thread<void (__cdecl&)(void)>(void (void) * _Fx) Line 49
mod.dll!initialize() Line 17
mod.dll!DllMain(HINSTANCE__ * __formal, unsigned long reason, void * __formal) Line 33

好的,std::thread 将“与其他线程同步”。

但是为什么呢?

我希望这在 VS2015 中不再发生,我还没有测试过。

【问题讨论】:

  • std 并不复杂。似乎 MSFT 的线程实现有问题。
  • 它试图与实际开始运行的线程保持良好并互锁。如果您有一个 1-800 的支持电话号码并且不特别喜欢来自未正确捕获其 lambda 参数的程序员的呼叫,您会倾向于做这种事情。使用调试器的 Debug > Windows > Threads 调试器窗口找出线程没有启动的原因。但是有些已成定局的结论是它也在尝试调用 DllMain() 入口点。加载程序锁死锁是一个标准错误。

标签: c++ visual-c++ msvc12


【解决方案1】:

std::thread 的规范包含以下要求 (N4527 §30.3.1.2[thread.thread.constr]/6):

同步:构造函数调用的完成与f副本的调用开始同步。

(其中f 是要在新创建的线程上执行的可调用实体。)

std::thread 的构造函数在新线程开始执行线程过程之前无法返回。创建新线程时,在调用线程过程之前,会为DLL_THREAD_ATTACH 调用每个加载的DLL 的入口点。为此,新线程必须获取加载程序锁。不幸的是,您现有的线程已经持有加载程序锁。

因此,你死锁了:在新线程开始执行线程过程之前,现有线程无法释放加载器锁,但新线程在获得由现有线程持有的加载器锁之前无法执行线程过程。

请注意,the documentation 明确建议不要从 DLL 入口点创建线程:

您不应该在DllMain 中执行以下任务:[...] 致电CreateThread。不与其他线程同步,创建线程也可以,但是有风险。

(该页面有一长串不应该从 DLL 入口点完成的事情;这只是其中之一。)

【讨论】:

  • 哦,这是规范的要求。所以我上面的代码是完全错误的:-(我知道死锁是如何发生的,但我不认为“如果你不与其他线程同步,创建一个线程可以工作,但它是有风险的”意味着“反对从DLL 入口点”,是的,理想的 DllMain 将只是一个空存根,但在某些情况下,它是进行初始化和清理工作的唯一机会。没关系,我将在这里使用 CreateThread。
  • @amanjiang 这句话显然是在谈论从 DLL 入口点创建线程,因为它是标题为“您永远不应该在 DllMain 中执行以下任务:”的列表的一部分。
  • “可以工作”和“不同步”就足够了。 “You should never perform the following tasks from within DllMain”列表中有很多东西,实际上列表是不完整的,例如 WSAStartup 不在列表中,我认为列表永远不完整。 DLLMain 有很多限制(包括所有 C++ 全局对象),只有一个线程可以“打破僵局”,虽然“有风险”,但这是希望。如果有一天在 DLLMain 中创建一个线程不能工作,那将是非常非常糟糕的一天,我祈祷那一天永远不会到来。
  • Doesn't "构造函数调用的完成与f的副本调用的开始同步"意味着"构造函数调用的完成"发生在"the开始调用 f" 的副本? (参见 [intro.multithread]/13.1, 14.2)“std::thread 的构造函数在新线程开始执行线程过程之前无法返回”似乎倒退了。
【解决方案2】:

您将平台级别与std 级别混合在一起。调用原始 winapi 函数CreateThread 可以在DllMain 中工作。但无法保证std::thread 将如何与平台交互。 It's well known that it's extremely dangerous to be doing things like this in DllMain,所以完全不推荐。如果您坚持尝试,那么您将需要小心翼翼地直接调用 winapi,避免 std 实现的后果。

至于“为什么”,这并不重要,但在调试器中快速查看后,似乎 MSVC 实现与新线程握手以交换参数和资源。所以它需要同步才能知道资源何时被移交。看起来很合理。

【讨论】:

  • “在 DLLMain 中创建线程”之类的东西并不危险,有时这是唯一的方法,因为 DLLMain 中的其他东西确实很危险。但似乎 std::thread 不仅仅是创建线程。
  • std::thread 绝对只是关于创建线程。但这不仅仅是关于CreateThread
  • 有时你无法区分“级别”。假设您是库的作者,在您的 API(或 ABI)背后,您有一些工作线程在后台执行异步工作。现在用户从 C++ 对象的构造函数调用您的 API,这将导致死锁。为什么 ?因为该对象是全局对象(或单例),并且它位于 DLL 模块中。所以 std::thread 将从 DLLMain 构造。
  • std是语言级别,语言级别很低。 std 不应该做太多假设。
  • Std 对 api 调用它可以和不能使用什么没有限制。如果您在平台 api 级别有技术要求,那么您就是不应该对 std 实现可以做什么做出假设的人。如果您需要在平台级别进行控制,那么您需要编写本机平台代码,而不是通过包括 std 在内的任何实现进行抽象。对不起。
【解决方案3】:

std::thread 创建一个 C++ 线程。这意味着您可以依赖该线程中的 C++ 库。这意味着必须设置某些共享数据结构,这会强制同步(您可能会并行创建多个线程)。堆栈跟踪清楚地表明了这一点:std::_Cnd_waitX 显然是标准库的一部分,并且显然正在同步。同步在你提到的文档中被列入黑名单,所以这次崩溃并不令人意外。

我们看到Concurrency:: 的堆栈更上一层楼。这特定于 Visual Studio 版本 up to VS2015。这意味着你可能会在 VS2015 中走运。在DllMain 中进行线程同步不是保证崩溃。很有可能。

【讨论】:

  • 我刚刚测试了它,我没有那么幸运。
【解决方案4】:

使用detach() 成员函数修复崩溃。示例:

void Hook_Init();

BOOL WINAPI DllMain(HINSTANCE hinstDLL, DWORD fdwReason, LPVOID lpvReserved)
{
    switch (fdwReason)
    {
    case DLL_PROCESS_ATTACH:
        {
            std::thread hookthread(Hook_Init);
            hookthread.detach();
            break;
        }
    }
    return TRUE;
}

void Hook_Init()
{
    // Code
}

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2023-01-08
    • 2013-01-20
    • 2021-05-26
    • 2013-07-24
    • 1970-01-01
    • 2018-04-20
    • 2012-09-20
    • 2018-06-04
    相关资源
    最近更新 更多