I'm working on a library which will be C API compatible.
Within the library there will be a global instance of an object which will have a std::thread
as a member. It seems that of some reason when the main
returns and exit()
is called, thread is automatically killed/terminated. This doesn't happen if the code is used within the same project (directly in the executable and not through a library).
I expect the following example to run in an infinite loop: while(1) {...}
and thread.join()
should block each other. It doesn't when it's with a library, the thread seems to have been already killed/finished when the destructor of CThread
is called. What am I missing here?
CThread.h
#ifndef CTHREAD_H
#define CTHREAD_H
#ifdef EXPORT_C_THREAD
# define EXPORT_CTHREAD __declspec(dllexport) __cdecl
#else
# define EXPORT_CTHREAD __declspec(dllimport) __cdecl
#endif
void EXPORT_CTHREAD testFunc();
#endif
CThread.cpp
#include "CThread.h"
#include <thread>
#include <memory>
class CThread
{
std::thread m_thread;
void infiniteLoop()
{
while (1) {
std::this_thread::sleep_for(std::chrono::microseconds(100));
}
}
public:
CThread()
{
m_thread = std::thread(&CThread::infiniteLoop, this);
}
~CThread()
{
m_thread.join();
}
};
std::unique_ptr<CThread> cthread;
void testFunc()
{
cthread = std::make_unique<CThread>();
}
main.cpp (this is in another project. I'm linking to the library above.)
#include "CThread.h"
int main()
{
testFunc();
return 0;
}
Update
As suggested I've tried to initialize the cthread
-object in the DllMain()
-function's during DLL_PROCESS_ATTACH
and deallocate during DLL_PROCESS_DETACH
. Since the DllMain()
-function will acquire the loader lock I have to initialize the thread later. However, as before, the thread is already aborted when DLL_PROCESS_DETACH
is "called". DLL_THREAD_DETACH
won't get called on exit.
Any more suggestions? Thanks!
答案 0 :(得分:1)
It seems that of some reason when the main returns and
exit()
is called, thread is automatically killed/terminated.
This is the expected behaviour of exit
function. I.e. it terminates the entire process and all its threads.
Within the library there will be a global instance of an object which will have a
std::thread
as a member.
It looks like this object is not created when it is compiled into a .dll
. You probably need to export that object explicitly, so that it is not optimized away because nothing in the .dll
refers to it, e.g.:
__declspec(dllexport) std::unique_ptr<CThread> cthread;
答案 1 :(得分:1)
I am able to reproduce this behaviour with VS2015.
The issue is with the std::unique_ptr<CThread> pthread;
being a global object. The deletion of the pointer is not tied to the execution of the main
thread. The global data of the dll and the global data of the host exe do not synchronise with each other in a predictable manner. There are further complications when introducing threads, these are stopped as the process exits.
As you have noted, moving all the code to a single exe allows for the global data to synchronise appropriately on the main
thread.
To work around this, you can either export an RAII styled class to manage the thread's execution or simply provide a "clear thread" or cleanup function and export that as well; the function would then have the following form;
void cleanupData()
{
cthread = nullptr; // block waiting for thread exit
}
To further assist client code, an RAII class can still be provided that supports this "clear thread", but it need not be exported from the dll.
The client side RAII could piggy back off of std::unique_ptr
or std::shared_ptr
as required. The simplest form would look like;
struct Cleanup {
Cleanup() = default;
Cleanup(Cleanup const&) = delete;
Cleanup& operator=(Cleanup const&) = delete;
Cleanup(Cleanup&&) = delete;
Cleanup& operator=(Cleanup&&) = delete;
~Cleanup() { cleanupData(); } // clean up the threads...
};
And this is required to be tied to the lifetime of the data begin used or imported from the dll.
And used as;
{
auto cleanup = std::make_unique<Cleanup>();
testFunc();
// ...
}
As an aside
In the destructor, before you join()
the thread, test to make sure it is joinable()
.
Given the update; can this be changed or improved, i.e. can the thread be controlled once the execution leaves main
? TL;DR, no.
Raymond Chen of "the old new thing" fame, quotes from here:
On the other hand, the C runtime library automatically calls
ExitProcess
when you exit themain
thread, regardless of whether there are any worker threads still active. This behavior for console programs is mandated by the C language, which says that (5.1.2.2.3) “a return from the initial call to themain
function is equivalent to calling the exit function with the value returned by themain
function as its argument.” The C++ language has an equivalent requirement (3.6.1). Presumably, the C runtime folks carried this behavior toWinMain
for consistency.
What does ExitProcess()
do?
...
- All of the threads in the process, except the calling thread, terminate their execution without receiving a
DLL_THREAD_DETACH
notification.- The states of all of the threads terminated in step 1 become signaled.
- The entry-point functions of all loaded dynamic-link libraries (DLLs) are called with
DLL_PROCESS_DETACH
....
In particular, points 1 and 3 above, by the time the destructor of std::unique_ptr<CThread>
runs, the thread is already stopped and signalled. You cannot rely on background thread execution once the main thread has called ExitProcess()
, they have already been stopped and the OS is in the process of cleaning up all the resources associated with the process.
You mention in the comments;
I will use an IPC mutex within the thread so it's critical that is cleaned up properly.
If the issue is a mutex and not just a thread, that changes the problem quiet a bit. It is easier then to extract the mutex to a global level and allow DllMain
to (with DLL_PROCESS_ATTACH
etc.) to manage it. The thread would be allowed access thereof as per normal. There may be some additional code to signal the state of the mutex, but that can all be managed with the dll as well.
Bear in mind as well that IPC mutex mechanisms usually include an "abandoned" state for this exact purpose. If the holder of the mutex fails unexpectedly and abandons the mutex, the remaining clients of the mutex are notified of this when the try access the mutex.
From the WIN32 Mutex:
If a thread terminates without releasing its ownership of a mutex object, the mutex object is considered to be abandoned. A waiting thread can acquire ownership of an abandoned mutex object, but the wait function will return
WAIT_ABANDONED
to indicate that the mutex object is abandoned...