A simple introduction to using C++ threads

Threads were introduced in C++ as part of the C++ 11 standard. It’s been a while, one should say! Using thread is pretty straightforward, yet beginners might face some challenges in managing the lifecycle of the thread objects. This article is intended to be used as a reference to use the threads in the right way! So, Let’s go ahead, create a thread and give it some task!

#include <iostream>
#include <thread>
using namespace std;
void foo() {
cout << "some work" << endl;
}
int main() {
thread t(foo);
}

But wait! This code crushes! A slight deep dive reveals that there is something called a joinable thread:

A thread object is joinable if it represents a thread of execution. A thread object is not joinable in any of these cases:

1. If it was default-constructed.

2. If it has been moved from (either constructing another thread object, or assigning to it).

3. If either of its members join or detach has been called.

As per these rules, the sample code, ours has a joinable thread. Also, the thread Destructor states that

If the thread is joinable when destroyed, terminate() Is called.

Which was crashing the above code! This means that we have to convert our thread object to a non-joinable thread object before letting it get destroyed. As per the rules above, there are 2 straightforward ways to do it

  1. join: Asks the parent thread to wait until the child completes its task

One thing to know here is, when a child thread is detached, one should be vary of the resources it is using. We could easily end up in a situation where the resources owned by the main thread (which will be destroyed with the main thread) are also being used in the child thread, causing undefined behavior when the child thread tries to access them once the main thread is destroyed. So, as a rule of thumb always prefer to use the join over detach, unless you know what you are doing! So, the code looks like this!

#include <iostream>
#include <thread>
using namespace std;
void foo() {
cout << "some work" << endl;
}
int main() {
thread t(foo);
t.join();
}

Now, let's pay attention to the work that is given to the thread. The constructor of thread is a variadic template, which means that it accepts any number of arguments. Here the arguments are not in the thread, but for the payload that is assigned to the thread. So one can do something like this:

#include <iostream>
#include <thread>
using namespace std;
void foo(int arg1, double arg2) {
cout << "some work : " << arg1 << " " << arg2 << endl;
}
int main() {
thread t(foo, 5, 2.0);
t.join();
}

You should always keep in mind if the arguments are being sent by copy or by reference. For example, while detaching the child from the parent thread, all the resources of the parent should be shared by copy since otherwise, we will have undefined behavior once the parent thread is destroyed.

Now, let's pay attention to what inputs can be given to the thread as payloads.

A pointer to function, pointer to member, or any kind of move-constructible function object (i.e., an object whose class defines operator(), including closures and function objects).
The return value (if any) is ignored.

So the payload given to a thread needs to be callable, i.e :

  1. Regular function

Now, there is one small concern in working with joinable threads, what happens if we get an exception between thread launch and thread.join()? In this scenario we will end up facing the same scenario as above, the destructor will be called on the joinable thread, to avoid this we can implement a simple wrapper class for the thread.

class scopedThread {
private:
thread m_thread;
public:
template <class Fn, class... Args>
scopedThread(Fn && fn, Args&&... args) : m_thread(fn, std::forward<Args>(args)...) {}
scopedThread(scopedThread &&other) {
m_thread = std::move(other.m_thread);
}
scopedThread &operator=(scopedThread &&other) {
m_thread = std::move(other.m_thread);
return *this;
}
std::thread &operator*() {
return m_thread;
}
std::thread const &operator*() const {
return m_thread;
}
std::thread *operator->() {
return &operator*();
}
std::thread const *operator->() const {
return &operator*();
}
auto join() {
if (m_thread.joinable())
m_thread.join();
}
~scopedThread() {
join();
}
};

This will make sure that the thread.join() call is made in every possible situation. using this class we can modify the code as follows:

#include <iostream>
#include <thread>
#include "scopedThread.h"
using namespace std;
void foo() {
cout << "some work" << endl;
}
int main() {
scopedThread t(foo);
}

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store