Modern Effective C++ - Make std::thread unjoinable on all exit paths.

std::thread can be in two states of joinability, joinable or unjoinable. std::thread usually serves as a handle on an underlying asynchronous execution which may either by running, blocked or waiting. Most of the threads are joinable. But there are some threads which cannot be joined i.e they are in an unjoinable state. The unjoinable threads include -

  1. Default constructed threads which was created without passing a function.
  2. The std::thread which has been moved. Just like any other variable which is moved, the std::thread which has been moved does not represent any underlying software thread and hence cannot be joined.
  3. A std::thread which is already joined.
  4. A std::thread which has been detached.

The below program demonstrates all these cases.


The reason why joinability is important is because,
  1. If join is called on an unjoinable thread, exception is thrown.
  2. If a destructor of a joinable thread is called, the program terminates immediately.

Here is the program to demonstrate it -



Case 2 of the above program is the main subject of this article. So let me re-iterate it again. We have to make sure all threads are in the unjoinable state before they are destroyed by the closest '}'. When some threads are in the unjoinable state by their very nature, most threads aren't. For those threads, we should make sure we call thread.join() before they are destroyed. When we can call thread_name.join() on these threads, the thread in a joinable state, "joins" the main thread, and goes into an unjoinable state.

Even though task based approach(std::async) is encouraged, there are some scenarios where creating std::threads make more sense. These scenarios include times where you have to get a native thread from std::thread, so that you can set it's priority.

In the below intentionally malformed program, we see that the thread is created to setup some resources for the paid subscribers of this program. However when the authorization fails, we still intend to continue with the execution of the free features of the program.  But it never happens.






This happened because once the authorize fails, we assume we do not have to wait for the resource initialization for the paid members to complete and hence never call the join. If we never call join, the std::thread t never goes into unjoinable state. When the computer reaches line 31 and starts destroying all the local variables (including std::thread t) is still joinable. The system is unable to decide the fate of this thread, and the program gets terminated as shown in the screenshot.

This program was pretty straightforward, and it was intentionally created to show how things can go wrong. In real world programs, some thread creating/originating functions (main is the thread creating function in above example) can have multiple exit paths, in the form of multiple return statements. A function can also exit/return abruptly when it throws an exception and it is not handled in the same function. So the challenge is to ensure calling std::thread.join() in all the exit paths of a thread originating function, including exception scenarios.

Whenever we need to do(or undo) a certain action(or set of actions) in all exit paths, we have to use RAII concept. We can build a RAII container class to encapsulate a thread (just like std::unique_ptr encapsulates raw pointer). This RAII thread container object calls join on the underlying thread when it is going out of scope. But we have to take care that we should only call join on threads which are in joinable state only. Here is a simple RAII container for threads and it's usage -




ThreadContainer is the RAII container. In the thread originating function i.e doMaths(), two threads are launched using ThreadContainer, and immediately we throw a random error. However in the runtime we see that the both the threads finish execution. This happens when the exception is thrown, the stack unwinding starts which calls the destructors of local variables. In our case, the local variables are thread containers. In the destructor of the thread container, we have called thread.join() on joinable threads. This forces the threads to complete before the control exits from the function doMaths(). The output of this program is -

 

If you comment line 41, 42 and uncomment from line 44 to 51, and run the program, you will see that the program crashes without any output.

The other important features of this program are -
  1. We can pass only rValues to ThreadContainer. 
  2. Since we have explicitly created destructor, the modern C++ compiler prohibits the automatic creation of copy and move operation . We use the default feature(lines 22,23) to explicitly support move operations.

This article is inspired by Scott Meyers's Effective Modern  C++. I highly recommend this book to anyone who wants to learn Modern C++ and it's nuances
Modern Effective C++ - Make std::thread unjoinable on all exit paths. Modern Effective C++ - Make std::thread unjoinable on all exit paths. Reviewed by zeroingTheDot on May 17, 2018 Rating: 5

No comments:

Powered by Blogger.