std::thread 在 DLLMain 中导致死锁

std::thread cause deadlock in DLLMain

所以,这就是我所说的: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 中创建线程是完全错误的吗?这不是真的。从 微软文档"Best Practices for Creating DLLs": "如果您不与其他线程同步,则创建一个线程可以工作 threads”。所以 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 会 "synchronize with other threads"。

但是为什么?

我希望在 VS2015 中不再发生这种情况,我还没有测试它。

您将平台级别与 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 实施的后果。

至于 "why",应该没什么大不了的,但在调试器中快速查看后,似乎 MSVC 实现与新线程握手以交换参数和资源。因此需要同步才能知道资源何时被移交。好像有道理。

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

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

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

Synchronization: The completion of the invocation of the constructor synchronizes with the beginning of the invocation of the copy of f.

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

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

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

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

You should never perform the following tasks from within DllMain: [...] Call CreateThread. Creating a thread can work if you do not synchronize with other threads, but it is risky.

(该页面有一长串不应从 DLL 入口点执行的操作;这只是其中之一。)

使用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
}