Sign in to follow this  
pseudomarvin

Exiting running threads in a DLL when calling executable exits

Recommended Posts

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.

Share this post


Link to post
Share on other sites

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.

Edited by phil_t

Share this post


Link to post
Share on other sites

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? 

Share this post


Link to post
Share on other sites

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();
    }

Edited by phil_t

Share this post


Link to post
Share on other sites

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?

Edited by pseudomarvin

Share this post


Link to post
Share on other sites

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.

Edited by Alberth

Share this post


Link to post
Share on other sites

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". Edited by rip-off

Share this post


Link to post
Share on other sites
Note that the behaviour you described does exist, it is just non standard. So in a way you are correct, once you qualify that it is compiler dependent. Now that the standard has thread support, there is little reason to rely on the older behaviour.

For example, Microsoft's C++ compiler documentation:

ISO Compliant
If you are familiar with the C# volatile keyword, or familiar with the behavior of volatile in earlier versions of Visual C++, be aware that the C++11 ISO Standard volatile keyword is different and is supported in Visual Studio when the /volatile:iso compiler option is specified. (For ARM, it's specified by default). The volatile keyword in C++11 ISO Standard code is to be used only for hardware access; do not use it for inter-thread communication. For inter-thread communication, use mechanisms such as std::atomic<T> from the C++ Standard Template Library.
End of ISO Compliant

Microsoft Specific
When the /volatile:ms compiler option is used—by default when architectures other than ARM are targeted—the compiler generates extra code to maintain ordering among references to volatile objects in addition to maintaining ordering to references to other global objects. In particular:

  • A write to a volatile object (also known as volatile write) has Release semantics; that is, a reference to a global or static object that occurs before a write to a volatile object in the instruction sequence will occur before that volatile write in the compiled binary.
  • A read of a volatile object (also known as volatile read) has Acquire semantics; that is, a reference to a global or static object that occurs after a read of volatile memory in the instruction sequence will occur after that volatile read in the compiled binary.
    This enables volatile objects to be used for memory locks and releases in multithreaded applications.

Share this post


Link to post
Share on other sites

Create an account or sign in to comment

You need to be a member in order to leave a comment

Create an account

Sign up for a new account in our community. It's easy!

Register a new account

Sign in

Already have an account? Sign in here.

Sign In Now

Sign in to follow this