std::thread
Syntax and Functionsjoin
or detach
detach()
While Using Localsstd::thread
Syntax and Functions#include <thread>
#include <iostream>
using namespace std;
void hello_world(){
cout << "hello world!" << endl;
}
int main(){
thread t(hello_world);
t.join();
assert(t.joinable() == false);
return 0;
}
In this case, we create a thread, bind a function to it,
and then wait for it to finish using join()
.
What if we called detach()
? Then even after the instantiation t
gets destroyed(after leaving its scope), the thread will still continue operations.
After join()
is done executing, we can guarantee that
t
is no longer associated with the actual thread, since
the thread’s execution actually finished.
This means, joinable()
will become false because
it’s only true for an active thread of execution.
join
or detach
What happens if we don’t call join()
or detach()
,
and just allow the thread’s destructor to get called?
Then in the destructor of t
, it will check for whether joinable()
, and if it is, it will raise std::terminate()
.
Now you must be thinking, why so violent? std::terminate()
should only be called in a few special circumstances like double exception propagation.
Say instead of terminate
, it just detach
’s the thread in the destructor.
What would happen?
We could be inevitably allowing undefined behavior as
the (destroyed) child thread could be using references
to the scope of which was already destroyed.
Thus, the designers of std::thread
thought termiante
was a necessary condition to avoid difficult UB-debugging.
We can’t allow a thread to not join()
in exception handling, so for example:
int main(){
thread t(my_func);
try{
do_something(); // exceptions could be called!
}
catch(...){
t.join();
throw;
}
t.join();
}
… which looks very ugly.
We can also implement a thread_guard
using RAII:
class thread_guard{
std::thread& t;
public:
explicit thread_guard(std::thread& t_) : t(t_) {}
~thread_guard(){
if(t.joinable())
t.join(); // force join here
}
};
...
int main(){
thread t(my_func);
thread_guard g(t); // RAII
do_something(); // exceptions could be called!
}
In this case, when main()
exits, ~g()
is called before
~t()
is called. Therefore, we will join()
on t before
the destructor of t
is called which could possibly
completely kill us.
When you detach()
a thread, the thread is usually called a daemon thread, which runs in the background with no way of communication.
A case study of this is creating a new document window in a GUI:
int main(){
while(true){
int command = get_input();
if(command == OPEN_NEW){
thread t(open_document);
t.detach(); // we create it and let it run.
}
}
}
detach()
While Using Localsvoid print(char* cp){
printf("%s\n", cp);
}
void foo(){
char buffer[1024];
sprintf(buffer, "hello world!");
std::thread t(print, buffer); // buffer is an argument.
t.detach(); // uh oh!
}
In the above situation, we passed in a local variable,
buffer
, which is used by the thread in print()
.
If we did a join()
, this is fine. However, we detach()
‘d.
This means if the following sequence occured:
detach()
gets called, buffer
is freed.printf()
is called with cp
.… then we have undefined behavior!
So keep in mind that when we’re detaching, never reference local variables.
The ownership model of std::threads
is very much like
std::unique_ptr
. You should use std::move
to move current thread context
from one RAII container to another.
thread t1(my_func);
thread t2 = std::move(t1);
After this execution, t1
now is not joinable()
, as it has no active thread context,
and t2
instead has the thread context.
What if we did this:
thread t1(my_func);
thread t2(my_func);
t2 = std::move(t1); // uh oh!
Here, t2
’s original active context will be removed, without join()
’s or detach()
’s.
We can only expect an std::terminate
here.