The strategy of thread ending (with a join for instance) is primordial for your multithreaded program.
Sometimes, on aged programs with large history, I had seen threads without join or/and any kind of destruction strategy.
A thread without any destruction strategy is extremely dangerous and can trigger issues that are painful to analyze! The consequences of threads without join are easy to show, let see this simple code:
#include <iostream> #include <thread> void threadFunction() { std::cout << "(THREAD) Hey, I am thread!" << std::endl; } int main() { std::cout << "(THREAD) Hey, I am main!" << std::endl; std::thread threadCreated(threadFunction); return 0; }
Let see the output...
(THREAD) Hey, I am main! (THREAD) Hey, I am thread! terminate called without an active exception Aborted (core dumped)
Here my application crashed. Why?
The response is that we exited the program before that "threadFunction" finished. A simple join added resolve this issue:
#include <iostream> #include <thread> void threadFunction() { std::cout << "(THREAD) Hey, I am thread!" << std::endl; } int main() { std::cout << "(THREAD) Hey, I am main!" << std::endl; std::thread threadCreated(threadFunction); threadCreated.join(); return 0; }
(THREAD) Hey, I am main! (THREAD) Hey, I am thread!
Here the main function waits for the thread termination before to exit the program. Now I am exiting properly my program.
Be careful
I want to keep your attention here. In this example, the consequences was easy to analyze and not dramatic.
Now, let's imagine this kind of issue in a large program. Imagine that a super class generates a thread.
What happens if this class is destroyed without join on the concerned thread?
There is an undefined behavior (in the best case, a crash during testing).
A thread not correctly managed can, in the long term, trigger those kind of behaviors:
Example of risks if a thread is not correctly exited
Nothing special, the thread exit by himself. However, if the design is not review, this case is dangerous and can evolve to the 2nd or worst, the 3rd case.
Crash... During the test, this is the best case for me. it is ideal to apply an action to this thread and review the design.
Memory modification, corruption... The worst one: Vicious, extremely dangerous and hard to detect.
During my carrier, I already saw the third case:
A crash (fortunately) occurred in the program, nothing special on the line when the crash happens. So, we started to analyze and debug the code. This debug took an entire week.
Finally, it was a detached thread which manipulated object supposed to be destroyed.
The resolution of those kind of issue are not easy to apply(Risk of deadlock, bad memory manipulation...). Fortunately, after few tentative, the risk of deadlock was limited and the bug had been fixed!
Always mastery your thread design
This last section is a simple message:
Always mastery your threads
A thread without any strategy of initialization and destruction can have terrible consequences on long term.
- A thread creation should not be done without design specification.
- A single thread application design is not always compatible for multithreading.
- A thread not correctly designed can manipulate destroyed objects.
A thread should always be destroyed properly. With or without join, it should be correctly designed to avoid surprises.
The multithreading is a wonderful tool, thanks to this, your application can be really reactive!
However, as I featured here, the question of multithreading in C++ should be taken with great caution..
In any kind of project, we should always be able to predict the threads behavior, in any cases and any states.
I hope that I was not too "judgmental" during this post. You certainly already apply those principles in your project.