C++ Threading by Examples — Part 2

Anubhav Rohatgi
4 min readOct 28, 2019

--

Previously we saw how threads are spawned and what the differences between a process and a thread are.

In this part of the C++ threading tutorial series we are going to look at the thread management and understand how to effectively manage thread using join and detach methods.

After the thread is started, it is imperative to conclude if the thread needs to be joined with the calling thread after it is done with the assigned task or it needs to run asynchronously as a daemon in a detached state. If we fail to decide before the thread destructor is called then by default the thread destructor calls std::terminate() after checking if the thread was still joinable.

Thread Join and Detach from main thread

std::thread::join() method waits for the thread task to finish, once that is done it helps in attaching the thread with the calling thread. Calling join() also cleans up any memory associated with the thread. join() forces the caller thread to wait until the called finishes up the task at hand.

struct func;void f() {
int local_state = 0;
std::thread t(func,local_state);
try {
do_something_in_current_thread();
} catch (...) {
t.join(); //joins current thread
throw; //throws some exception
}
t.join(); //joins current
}

The above code ensures that a thread with access to a local state is finished before the function exits whether normally or by an exception.

std::thread::detach() method does not check if the task which is being processed inside the thread is completed or not, it straightaway detaches the thread from the caller thread and runs in the background as a daemon process for e.g. tabs in chrome browser session, document windows in a adobe GUI and so on.

std::thread::joinable() method tests if thread object is joinable before calling the join().

Double join() or detach() will result in program termination. Therefore we should always check if the thread is joinable using std::thread::joinable() before joining or detaching the thread. If an thread object is no longer joinable, joinable() will return false.

//Basic Example of Detach and Join thread methods
void longProcess() {
std::this_thread::sleep_for(std::chrono::seconds(5));
std::cout<<"\nCompleted Long\n";
}
void shortProcess() {
std::this_thread::sleep_for(std::chrono::seconds(2));
std::cout<<"\nCompleted Short\n";
}
int main(){ std::thread t1(longProcess);
std::thread t2(shortProcess);
t1.join();
t2.detach();
std::cout<<"\nCompleted\n";}

Double Join() or Detach() Error

std::thread t1(longProcess); //create and launch threadt1.join(); // 1st call to join() waits for the thread 
// to finish and join the caller thread
t1.join(); // 2nd call to join() the caller thread
// throws an exception and terminates as
// the thread has already joined the caller thread
std::cout<<"\nCompleted\n";
Double join or double detach throws an error

Thus to avoid the above problem, we use joinable() method to check if the thread is available for joining or not.

std::thread t1(longProcess);if(t1.joinable()) //checks is the thread is joinable
t1.join();
std::cout<<"\nCompleted\n";

But some threads are Unjoinable

  1. A Thread which is already in a detached state.
  2. A Thread which is already joined.
  3. A Thread which was constructed without passing a callable type e.g std::thread t;
  4. A Thread which has been moved.
std::thread t1(shortProcess);
std::thread t2(std::move(t1));
// t1.join() will throw exception and terminate
t2.join(); //runs perfectly

Handling join() using Resource Acquisition is Initialisation (RAII)

Thread management using RAII idiom

In this case, when main() exits, ~threadRAII() gets called before ~t1() is called even when the function exits and do_something() throws an exception. The destructor of threadRAIIfirst tests to see if the std::thread object is joinable() before calling join() . This is important, because join() can be called only once for a given thread of execution.

work1::work1(int) Ctor : 1
work1::work1(const work1&) 1
work1::work1(const work1&) 1
work1::~work1() Dtor : 1
work1::~work1() Dtor : 1
void work1::operator()() Printing this :: 0
void work1::operator()() Printing this :: 1
void work1::operator()() Printing this :: 2
void work1::operator()() Printing this :: 3
void work1::operator()() Printing this :: 4
work1::~work1() Dtor : 1
terminate called after throwing an instance of 'char const*'

Avoid local variables when Detach() ing from a thread

When sharing a variable pointer make sure it does not go out of scope of the calling thread before the called thread is completed. For e.g. :

struct info{
std::string name;
int age;
info(std::string nm, int ag) : name(nm),age(ag){
//ctor
}
};
void greet(info* n) { std::cout<<"\nName : "<<n->name<<" Age : "<<n- >age<<std::endl;
}
void meet(info* n) {
delete n;
n = nullptr;
}
int main() {
info* n = new info("anubhav",30);
std::thread t1(greet,n);
std::thread t2(meet,n);
t2.detach();
t1.join();
return 0;
}

In the above case n is a local variable to main() . It is passed to thread t1 and t2, greet prints the info and meet releases the info. It may happen that while t1 is trying to print the info concurrently info gets deleted by t2 resulting in dangling pointer and forcing t1 to print nothing. Hence resource sharing, scope and lifetime of the variable becomes an utmost important aspect of multi-threading paradigm.

Key Takeaways

  • Pre-determine what needs to be done with the thread either detach or join.
  • Avoid double detach or double joins on a single thread.
  • Let RAII manage threads.
  • Join will force the calling thread to wait for the called thread to complete the task and then fuse with the calling thread.
  • Care must be taken while handling global variables being accessed by multiple threads

--

--