
Chapter 2 - Part 1. Managing Threads

If not specified, all the code is written in c++23 standard, compiled with g++ 13.2
command: g\++ -std=c\++2b -O0 ch2.cpp -o out && ./out
Chapter 2. Managing Threads - Part 1
Basic thread management
A thread is wrapped in a std::thread
object and in the constructor we can specify the task that the thread will run. After the function returns, the thread ends.
Here’s how to construct and start a thread:
1 |
|
std::thread t(hello)
created a std::thread
object that will execute the function hello
and start it. And the t.join()
will make the code wait for the thread to finish.
Output:
1 | Hello Concurrent World |
We can of course also overload the ()
operator to make an object callable. Here’s an example:
1 |
|
The code above will output:
1 | Hello Concurrent World from object! |
Similar to the syntax of std::bind
, std::thraed
can also be constructed with a member function address and the corresponding object:
1 |
|
The output is:
1 | Hello Concurrent World from member function! |
The first parameter is the member function address, and the second parameter is the object address that the member function will be called on, similar to the syntax of std::bind
.
icon-padding
All cpp program has the “main” thread started by cpp runtime. We can also define our own thread, and it will start when it’s defined.
Wait for it… or not?
After we start a thread, if we don’t explicitly state whether we will wait for it to finish or not, the thread will
We can wait the thread to finish by using thread.join()
method. This will block the current thread when the code reaches thread.join()
until the thread we are waiting for finishes.
1 |
|
Output:
1 | Thread start |
At line 12, we started the thread and Thread start
is printed. Then we wait for 1000 milliseconds and print Thread end
. After that, we print finished
and the program ends. From this we can see the t.join()
actually blocks the main thread until the thread t
finishes.
We can also forget about the thread object, don’t wait for it, and let it run.
1 |
|
Output(machine dependent):
1 | finished |
Which is expected. After the thread t was created, the main thread keeps running, printing finished
. Since creating a thread takes time, the Thread start
is printed after finished
and before program finishes.
Potential problem about detached thread
Well, the problem is intuitive to think about. If we are using an object that is passed to the thread. And the object has a reference variable that is referencing something on the stack. When the variable the member variable is referencing goes out of scope, a dangling reference occurs and leads to unexpected and undefined behavior.
Here’s an example:
1 |
|
Output:
1 | 42 |
Which is clearly not what we expected. Since the thread is detached at line 24, the function won’t wait the thread to finish. Thus, when a
in wtf
goes out of scope, the thread is still running. Therefore, at line 13, data
is the reference to a destroyed object.
Thus, when a thread only need to read but not modify the data, it’s the best to pass in a copy of the data not the reference to a variable in another scope. Thus, if the original data is destroyed, the object is still there and there will be no dangling references.
Exceptional situation
In some cases, the thread.join()
won’t be called, like when there’s an exception and the scope terminated early:
1 |
|
In this case t.join()
at line 8 won’t be executed because this function terminated before the position we want the thread to join. Therefore, this thread won’t end after this program finish and will result in the same problem as detached thread.
One way to resolve this is to use try-catch statement:
1 |
|
In this case, no matter what happens, the thread will be joined, and the thread will end normally whether there is an exception or not.
Another way is to use the concept of RAII(Resource Acquisition Is Initialization). To do it, we define a ThreadWrapper
class:
1 | class ThreadWrapper{ |
icon-padding
We wish to operate the thread directly, so we are getting the reference of the thread object. And in the destructor, we check if the thread is joinable, if it is, we join it. And we also delete the copy constructor and copy assignment operator because one thread should not be duplicated by default.
Then we can use it like this:
1 |
|
In this case, even there is an exception and code terminates early, the destructor of ThreadWrapper
will be called, and the thread will be joined.
Detached threads
A detached thread always run in the background with no direct means of communication, and is often called a daemon thread. Most of the time, a detached thread will run till the program ends.
If a thread is detached, its ownership is transferred to the cpp runtime and no longer have association with current thread (since it’s ‘detached’). Therefore, we can’t join it or detach it again.
In the class ThreadWrapper
, before we try to join the thread, we check the if the thread is joinable by calling t.joinable()
and if it’s true, we join it.
A thread that is waiting to be joined can also be detached by calling detach()
method (but remember to use joinable()
to make sure when the thread is joinable at join()
position). Since both join()
and detach()
requires the object to have a thread associated with it, and we can communicate with it. Therefore, if joinable()
of a thread object returns true, we can call join()
or detach()
on it.
Function parameters when calling thread
For functions with parameters, we can pass the parameters in the constructor of std::thread
object:
1 |
|
The output is:
1 | 0 |
Potential problems about passing parameters
The problem could occur might not be so obvious. Suppose we are passing a value that will be implicitly cast to another type (for example a char* to a string):
1 |
|
The problem is not obvious in the first place, but when the thread starts and the parameter is passed to the thread, the thread will have to copy the char*
and implicitly cast the char*
to std::string
. Since the thread is detached, because the constructor is only copying s
, which takes time, the implicit cast might occur after s
goes out of scope because the constructor doesn’t do any implicit casting, leading to undefined behavior (dereferencing pointer pointing to freed memory).
One way we can fix this problem is to cast the parameter to the type we want before passing it to the thread:
1 | void work(const std::string& s){ |
In this case we are constructing a std::string object
while s
is still in the scope, thus we don’t have to worry about scope of variables.
Another problem is related to the default behavior of std::thread
constructor. By default, the constructor copies the parameter into the thread, which means even though we define the parameter as a reference, it will still produce a copy and the reference will be referencing the copy rather than the original object.
Whether it’s a copy or not can be checked by comparing the address of them:
1 |
|
The output is:
1 | original:0x16ef16ccc |
Even more weird happen when we remove the const
modifier:
1 |
|
this code actually won’t compile, part of the error message is:
1 | /opt/homebrew/Cellar/gcc/13.2.0/include/c++/13/bits/std_thread.h: In instantiation of 'std::thread::thread(_Callable&&, _Args&& ...) [with _Callable = void (&)(int&); _Args = {int&}; <template-parameter-1-3> = void]': |
Take a look at the error message and the header file std_thread.h
:
1 | template<typename _Callable, typename... _Args, |
In the first case, we have a const lvalue reference to the copied and moved rvalue reference, so the code compiles, but we don’t get the desired result.
In the second case, the template__is_invocable<typename decay<_Callable>::type, typename decay<_Args>::type...>::value
is used to check if the parameter is invocable after conversion to rvalues. Because we cannot pass a rvalue reference to a non-const lvalue reference, this will be evaluated to false, failing static_assertion
and fail to compile.
The solution will be wrapping the parameter in std::ref
:
1 |
|
By doing so, the parameter will be passed correctly as a reference rather than a copied a rvalue.
The output is:
1 | original:0x16afdacc4 |
Meaning we are passing the original reference to the thread.
See more in next part~