6

So, this is what I'm talking about: std is complex.

In VS2013 this simple program will cause a deadlock.

#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;
}

Create a thread in DLLMain is totally wrong ? It's not true. From the document "Best Practices for Creating DLLs" of Microsoft: "Creating a thread can work if you do not synchronize with other threads". So CreateThread works, _beginthreadex works, and boost::thread works, but std::thread will not work. This is the call stack:

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

Okay, std::thread will "synchronize with other threads".

But why ?

I hope this never happens again in VS2015, I didn't test it yet.

amanjiang
  • 1,021
  • 12
  • 30
  • It is not that std is complex. It just seems MSFT implementation of thread is buggy. – SergeyA Aug 27 '15 at 14:36
  • It tries to be nice and interlock with the thread actually starting to run. The kind of thing you'd tend to do if you have a 1-800 support phone number and don't particularly enjoy calls from programmers that don't capture their lambda arguments properly. Use the debugger's Debug > Windows > Threads debugger window to find out why the thread isn't starting. But with the somewhat foregone conclusion that it is also trying to call a DllMain() entrypoint. Deadlocking on the loader lock is a standard bug. – Hans Passant Aug 27 '15 at 15:14

4 Answers4

8

The specification for std::thread contains the following requirement (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.

(where f is the callable entity which is to be executed on the newly created thread.)

The constructor for the std::thread cannot return until the new thread starts executing the thread procedure. When a new thread is created, before the thread procedure is invoked, the entry point of each loaded DLL is invoked for DLL_THREAD_ATTACH. To do this, the new thread must acquire the loader lock. Unfortunately, your existing thread already holds the loader lock.

Thus, you deadlock: the existing thread cannot release the loader lock until the new thread starts executing the thread procedure but the new thread cannot execute the thread procedure until it can acquire the loader lock, which is held by the existing thread.

Note that the documentation expressly recommends against creation of threads from the DLL entry point:

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.

(That page has a long list of things that should not be done from a DLL entry point; this is just one of them.)

James McNellis
  • 327,682
  • 71
  • 882
  • 954
  • Oh, it's the requirement of specification. So my code above is totally wrong :-( I know how the deadlock happened, but I don't think "Creating a thread can work if you do not synchronize with other threads, but it is risky" means "against creation of threads from the DLL entry point", Yes, The ideal DllMain would be just an empty stub, but in some cases it's the only chance to do initialization and cleanup job. It's ok, I will use CreateThread here. – amanjiang Aug 28 '15 at 14:59
  • 1
    @amanjiang The sentence clearly is talking about creating threads from the DLL entry point, because it's part of a list whose title is "You should never perform the following tasks from within DllMain:". – Raymond Chen Sep 18 '15 at 20:05
  • "can work" and "do not synchronize" are good enough. There are a lot of things in the list of "You should never perform the following tasks from within DllMain", and in fact the list is incomplete, for example WSAStartup is not in the list, I think the list is never complete. There are so many limits of DLLMain (including all of C++ global objects), only a thread could "break the deadlock", although "it is risky", but it's the hope. If someday create a thread in DLLMain can't work, that would be a very very bad day, I pray that day never comes. – amanjiang Sep 21 '15 at 17:03
  • Doesn't "the completion of the invocation of the constructor synchronizes with the beginning of the invocation of the copy of `f`" mean "the completion of the invocation of the constructor" happens before "the beginning of the invocation of the copy of f"? (See [intro.multithread]/13.1, 14.2) "The constructor for the std::thread cannot return until the new thread starts executing the thread procedure" seems backwards. – T.C. Nov 22 '15 at 10:00
0

You're mixing the platform level with std level. Calling the raw winapi function CreateThread can work in DllMain. But there's no guarantee about how std::thread will interact with the platform. It's well known that it's extremely dangerous to be doing things like this in DllMain, so I don't recommend it at all. If you insist on trying, then you're going to need to tip-toe around and call the winapi directly, avoiding consequences of the std implementation.

As for "why", it shouldn't really matter, but after a very quick look in the debugger, it seems that the MSVC implementation has a handshake with the new thread for exchanging arguments and resources. So it requires synchronization to know when the resources have been handed off. Seems reasonable.

tenfour
  • 33,679
  • 12
  • 73
  • 135
  • Thing like "create a thread in DLLMain" is not dangerous, sometimes it's the only way, because other things in DLLMain is really dangerous. But it seems std::thread is not only about creating thread. – amanjiang Aug 27 '15 at 15:25
  • 1
    `std::thread` is definitely only about creating threads. But it's not only about `CreateThread`. – tenfour Aug 27 '15 at 15:26
  • Sometimes you can't distinguish the "level". Let's say you're a author of a library, behind your APIs(or ABIs), you have some worker threads to do the asynchronous works in the background. Now an user calls your API from a C++ object's constructor, this will cause a deadlock. Why ? Because that object is a global object (or singleton), and it's in a DLL module. So std::thread will be constructed from DLLMain. – amanjiang Aug 28 '15 at 01:02
  • std is language level, language level is very low. std shouldn't make too many assumptions. – amanjiang Aug 28 '15 at 01:10
  • Std has no restrictions on what api calls it can and can not use. If you have technical requirements at the platform api level, then you are the one that shouldn't be making assumptions about what the std implementation can do. If you need control at the platform level, then you need to write native platform code and not abstract through whatever implementation including std. Sorry. – tenfour Aug 28 '15 at 08:45
  • What you've said is reasonable. Maybe every C++ programmer who is writing the real-world applications has his own "technical requirements", and he will write his own std. And maybe this is the reason that many large softwares and video games have self- build infrastructures. So std is just a training aid. – amanjiang Aug 28 '15 at 09:45
  • I don't understand your objections here. Are you saying that every std call should be bound to only call specific platform api calls? That would be impossible and useless. In almost every situation, you are not facing restrictions like in dllmain. It's exceptional, and the std behaviour seems reasonable to me. And indeed when you have limitations on the platform api you can use, you typically avoid std. For example embedded systems. This whole conversation is the consequence of c++ being a high-level language. I'm curious that if you see this as a problem, what your solution would be. – tenfour Aug 28 '15 at 10:22
  • Never mind, it doesn't matter anymore. Yes, I think std should be straightforward. If possible, a "thin wrapper" of Win32 API will be good. If not possible, keep it simple. In this way it will be more useful in real-world. – amanjiang Aug 28 '15 at 15:18
0

std::thread creates a C++ thread. It means you can rely on the C++ library in that thread. This means certain shared data structures must be set up, which force synchronization (you might create multiple threads in parallel). The stack trace clearly shows this: std::_Cnd_waitX is clearly a part of the Standard Library, and is clearly synchronizing. Synchronizing is blacklisted in the document you mentioned, so this crash isn't a big surprise.

Further up the stack we see Concurrency::. This is specific to Visual Studio versions up to VS2015. This means you may luck out in VS2015. Doing thread synchronization in DllMain is not a guaranteed crash. Just quite possible.

MSalters
  • 159,923
  • 8
  • 140
  • 320
0

Use detach() member function to fix the crash. Example:

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
}
Naydef
  • 1