Exiting running threads in a DLL when calling executable exits

Started by
9 comments, last by rip-off 8 years, 6 months ago

My D3D application is divided into two parts: win specific platform code running in the executable and platform independent code running in a DLL which is linked with the exe. So most of the interesting stuff happens in the DLL. Threads can be spawned in the DLL to handle certain computations which can last up to 5 seconds.

When the app exits and the computation is still taking place in a spawned thread in the DLL I get a "Application has stopped responding" error, I assume because the threads were not exited properly.

What is the proper approach to handle this? Should the executable on exiting notify the DLL and give it time to shut the threads down? In that case should I keep track of running threads in the DLL (by collecting running threads in an array for example, no idea if that is possible) and close them down upon exiting?

I'm using std::thread.

Thanks.

Advertisement

What is the proper approach to handle this? Should the executable on exiting notify the DLL and give it time to shut the threads down?

That's what I would do. Hopefully your API would expose some shutdown function that the consumer is responsible for calling, or you have some other way to detect this. Even if your code were built directly into the exe, you still have this problem.

Thanks. As a follow-up: Suppose I do not use any higher level API above std::threads. Is there a smart way I can track them from creation onwards and perhaps later notify them from the main thread to let them know that they should exit? Or it doesn't work like that.

E.g., when I currently create these computation threads it looks like this:


std::thread treeModelThread(ReloadTreeModel, context, grow);
treeModelThread.detach(); 

Then function ReloadTreeModel() now runs until it is over and then the thread dies. Once detaching I lose control of the thread. Can I keep it somehow?

Sure, you delete the call to detach, then you keep it.

Later when you want to quit the program you can tell the thread to quit (for example using an atomic flag) and join it.

Suppose you have some global variable, or member variable in an instance that is guaranteed to be kept alive until your API is being shutdown:


std::vector<std::thread> myThreads;

And then you create your threads as needed, and add them to that vector so they don't go out of scope (this ends up using the move constructor in c++11):


    myThreads.push_back(std::thread(ReloadTreeModel, context, grow));
    myThreads.push_back(std::thread(ReloadPersonModel, context, grow));
    // etc....

Then when it comes time to shutdown:


    // TODO: Notify threads that they should exit... (say, by atomic_flag, like wintertime suggested)
    // Now wait for the threads:
    for (auto &thread : myThreads)
    {
        thread.join();
    }

Thanks for the suggestions, it works now without the error. I have another follow-up: Suppose one of the threads is at the beginning of a 5-second computation when the exit command is given by the user. How can I tell the thread to stop computation and exit immediately?

The only way I can think of is for the thread to periodically check an atomic flag as you suggested, but the execution will be deep in a different module than the one where the thread was created. I could pass the atomic flag as a parameter there and check it periodically but this whole approach seems a bit cumbersome. Is there a better way to do it?

Also, performance-wise if the flag is checked only perhaps once every 0.2 seconds, it's almost like it didn't happen right?


Suppose one of the threads is at the beginning of a 5-second computation when the exit command is given by the user. How can I tell the thread to stop computation and exit immediately?

You don't. The only way is to have the thread poll some variable to notify the condition, and allow it to shutdown gracefully. A thread may have resources claimed, you can't just pull it down when you want.

It's just like you start a long computation, and after starting it you change your mind. You can't just say "stop now", you have to close down the application, press CTL+C, or whatever. That works, as the operating system then handles resource release. With a thread, you don't get that service.


could pass the atomic flag as a parameter

A plain boolean is enough. No need to make it atomic as the thread doesn't change it. It may get an inconsistent state or miss a check, but it will be picked up the next time.

Locking and unlocking also take time.

As for the period, I'd make it even longer, eg 0.5 to 1 second, but it depends on desired response time.

Edit: You should make the boolean volatile, to avoid the compiler optimizing the access away.

Great, thanks for your help.

A plain boolean is enough. No need to make it atomic as the thread doesn't change it. It may get an inconsistent state or miss a check, but it will be picked up the next time.

Unfortunately not. Without some kind of thread-safe modifier, the compiler is allowed to assume single-threaded behaviour, if it cannot see the variable being modified in a given area of code, it can assume the valaue never changes - as such, changes in thread A may *never* appear to thread B.

Locking and unlocking also take time.

True in general, but note that atomic operations don't actually lock or unlock, though you could build such behaviour on top of them.


Edit: You should make the boolean volatile, to avoid the compiler optimizing the access away.


Volatile is quite different from thread safety. It says to the compiler "make no assumptions about the current state of this variable, it could be changed at any time (e.g. by a hardware peripheral)".

While it is true that some compiler vendors have extended the meaning of "volatile" to be used for inter-thread communication, that was due to the absence of a standard alternative. Adding the thread-safe primitives to the standard was supposed to replace such non-standard extensions.

If you're worried about the cost of atomics, volatile should be much worse as it requires the current value to be fetched from real memory, bypassing all CPU cache levels in between. Atomic operations can work within the caches, though there is a cost to whatever inter-core cache consistency messaging protocols are used by the processor.

I can't remember the exact details now, so please double check this, but I believe there is an additional issue using "volatile", which is that the compiler and processor are still allowed interleave other instructions relative to the volatile memory access which can result in thread-safety issues. However, thread safe atomic operations have very strict ordering rules which the compiler must respect, and it needs to emit whatever memory barrier instructions which may be necessary to ensure the processor doesn't re-order incorrectly either. I believe this is the kind of non-standard guarantee that certain compiler vendors would make about "volatile".

Thanks for pointing out my errors; it seems I should be even more careful with thread communication.

This topic is closed to new replies.

Advertisement