Ensure Join or Detach Before Thread Destruction in C++
Introduction
When we write multithread C++ programs, we might be curious why we always have to call join()
or detach()
for the new threads. In some scenarios, if we forgot to call join()
or detach()
for the new threads, there might be peculiar program crashes which are hard to troubleshoot.
In this blog post, I would like to discuss the consequence of not calling join()
or detach()
for the new threads, and some of the methods that could make sure that join()
or detach()
for the new threads are always called.
Consequences for No Join or Detach
The consequence of not calling join()
or detach()
for the new threads is program termination due to std::terminate
was called.
Let’s check the following example. The main function in the main thread instantiated a new_thread
which sleeps 5 seconds, then the main thread itself sleeps 1 second before it goes out of the scope.
1 | // g++ -std=c++14 example.cpp -lpthread -o example |
We compiled the program using the following command.
1 | $ g++ -std=c++14 example.cpp -lpthread -o example |
When we executed the program, the program crashed.
1 | $ ./example |
This is because, when the execution of the main function finished, the destructor of new_thread
will be automatically called for garbage collection. In the description of the destructor std::thread::~thread
, If *this
has an associated thread (joinable() == true
), std::terminate()
is called. If the std::thread
object has been called with join()
or detach()
, joinable()
would become false
, otherwise it is always true
. Because new_thread
had not been called with join()
or detach()
before its destructor was called, joinable()
was true
and std::terminate()
was called and the C++ runtime was killed.
Although it terminates the program brutally, such design has its rationale. Without the std::terminate()
mechanism in the destructor std::thread::~thread
, if the users wanted to do join()
, but forgot to call join()
, the new_thread
will run in the background just like the detach()
behaviors. This might cause undefined behaviors.
Caveats
In addition to forget writing join()
or detach()
in the code, which is easy to find out, sometimes we have written join()
or detach()
in the code but they were not called due to exceptions. This will also results in program crash and might be difficult to troubleshoot.
The most conventional way is to add join()
or detach()
in the catch
block.
1 | // g++ -std=c++14 example.cpp -lpthread -o example |
There are other ways to deal with the thread issue, such as using RAII.
We could create an instance of ThreadGuard
right after the std::thread
object is created. This instance of ThreadGuard
would make sure the std::thread
object always call join()
or detach()
before going out of scope.
1 | // g++ -std=c++14 example.cpp -lpthread -o example |
Implementing some std::thread
wrapper is also not a bad idea. Boost has a ScopedThreads
library whose wrapper implementation is similar to the following ScopedThread
class.
1 | // g++ -std=c++14 example.cpp -lpthread -o example |
Notes
Although program termination during C++ program development and test helps us to find out the missing join()
and detach()
during runtime, if the program is not well tested and the missing join()
and detach()
go into the deployment, it will be a big headache to realize there is such a problem. We don’t want our program to crash during deployment! Try to think of all the possible scenarios, catch the exceptions and deal with them appropriately!
References
Ensure Join or Detach Before Thread Destruction in C++
https://leimao.github.io/blog/CPP-Ensure-Join-Detach-Before-Thread-Destruction/