Introduction
A multithread or concurrent code at some point runs multiple tasks simultaneously. For example, an app can run some calculations with one thread in the background while the GUI is still responsive with another thread. C++ provides thread
library to write multithread codes.
Prerequisites
I assume you are familiar with the difference between parallel and concurrent code.
I am using compiler GCC 11 with flags of -std=c++20 -pthread
. The headers and main function might be missing in some examples:
#include<iostream> // for std::cout
#include<thread> // for std::thread
using std::this_thread::sleep_for;
using std::chrono::seconds;
int main(){
// examples are here.
return 0;
}
Definition
A thread object can be defined with
std::thread t(aFunction);
The moment a program reaches this line, a task is started in the background to run aFunction
.
t
is the thread object,aFunction
is a task or thread execution.
Thread object is the only tool that connects us to the background process.
Let’s define a function that takes 7 seconds to run.
auto f(){
std::cout<<"Task started ...\n";
sleep_for(seconds(7));
std::cout<<" Task done!\n";
}
The function is run in the background like:
int main(){
std::thread t(f);
std::cout<< "Run some other tasks.\n";
t.join();
return 0;
}
The order of output on my laptop is
Run some other tasks.
Task started ...
Task done!
So when the program reaches std::thread t(f)
,
- it starts a new thread to run function
f()
. - The new thread runs in the background parallel to the main program thread.
- The main program immediately moves to the next line
std::cout<<"Run..."
.
You can make as many threads as you like to run in the background:
std::thread t1(task1);
std::thread t2(task2);
std::thread t3(task3);
where task1
, task2
, task3
are functions.
We can wait for a background thread to finish its job by using join()
function:
t.join();
Always remember to join running thread objects before they get destructed:
auto RunTaskparallel(){
std::thread t1(task1);
std::thread t2(task2);
// t1 & t2 joined before destruction.
t1.join();
t2.join();
}
If you don’t join running thread objects and they are destructed, the program is terminated i.e. C++ calls terminate()
function.
However, do not join an empty thread:
std::thread t;
t.join(); // System Error exception: no task is running.
Pass function parameter
Function parameters are passed to a thread object constructor after the name of the function:
auto task(int i, std::string s){/*...*/}
std::thread t(task, 1, "Hi");
Move but not copy
Thread objects are not copyable:
auto task(){/* some computation */}
std::thread t1(task);
std::thread t2 = t1; //Error: do not copy threads
std::thread t3{t1}; // Error: do not copy-construct threads
Therefore, only one thread object is responsible for one task at a time. However, the task associated with a thread object is movable:
auto task(){/* some computation */}
std::thread t1(task);
std::thread t2 = std::move(t1);
Now t1
is an empty thread object and the running task in the background is now associated with t2
. Therefore, join t2
to wait for computation not t1
.
And of course, you can pass the task out of a function by returning by value (a hidden std::move()
happens):
auto task(){/* some computation */}
auto RunParallel(){
std::thread t(task);
return t;
}
// Task running and s is associated with it:
auto s = RunParallel();
Detach thread
A thread object can be detached from its task. This is good for a task that needs to be run in the background and we don’t need to stop it during runtime. An example of non-stop task would be checking a currency price every second:
auto CheckBtcUsd(){/*some code*/}
auto StartBtcUsd(){
std::thread t(CheckBtcUsd);
t.detach();
}// t is destructed here
// but CheckBtcUsd() is running.
Joinable
Sometimes we need to know if a thread object is joinable i.e. it is associated with an active task. We can assess it with joinable()
function:
if (t.joinable())
t.join();
A thread object is NOT joinable if:
- the thread object is default constructed (with no task) like
std::thread t;
- the thread is already joined or was detached before,
- the task is moved from the thread object.
Note that if a thread is joinable
, it doesn’t mean it is finished. It may be still running. So, if we call join()
on a joinable
task it joins immidietly only if it is already finished, otherwise, the program waits for it to finish and then it joins.
Thread for class
The syntax for class function is different to standalone function.
std::thread t(&ClassName::FunctionName, ClassObject, functionParameters...);
See this example:
class Player{
public:
auto playTask(){/**/}
auto play(){
// Thread inside class
std::thread t(&Player::playTask, this);
t.detach();
}
auto search(){/**/}
};
Player p;
// thread outside class
std::thread t(&Player::search, p)
Exception
Threads are not RAII, we need to join or detach a joinable thread at some point otherwise the program is terminated. This creates problems if an exception is thrown before joining:
auto task(){}
auto RunTask(){
std::thread t(task);
/* a code that may
throw exception */
t.join();
}
In the above code t.join()
is not reached if an exception is thrown before that. To solve this you can do this:
auto RunTask(){
std::thread t(task);
try{
/* a code that may
throw exception */
}
catch(...){
t.join();
throw;
}
t.join();
}
You can also create a RAII class that wraps std::thread
. When the class is destructed, it joins the thread automatically:
class RaiiThread {
private:
std::thread& t;
public:
RaiiThread(std::thread& _t ) : t(_t) {}
~RaiiThread() {
if(t.joinable())
t.join();
}
//Thread cannot be copied or assigned
RaiiThread(const RaiiThread &)= delete;
RaiiThread& operator=(const RaiiThread &)= delete ;
};
Where to use
Threads are popular in higher-level programs. Besides C++, they are available in C#, Java, Python, and so on. They are usually used for sending/receiving network requests, I/O operations, running background processes of a graphical user interface (GUI), communicating with external devices, and so on. Usually, the number of concurrent tasks is unknown at the start of runtime and depends on the user interaction with the software. For example, a database server may not get any requests at 4:00 AM but gets 30 concurrent requests at 11:00 AM. Therefore, the number of concurrent tasks can exceed the number of CPU cores, and OS juggles them.
If you are looking for high-performance parallel processing where the number of processes is known at the start of runtime, I recommend checking OpenMP library to utilize CPU cores of a machine, OpenACC for GPUs, and MPI for supercomputers.
Workshop example
In a workshop, a boss has two carpenters who make coffee tables. Here, the boss is the main program, and the carpenters are background threads:
#include <iostream>
#include<thread>
#include<chrono>
using std::this_thread::sleep_for;
using std::chrono::seconds;
using namespace std;
auto MakeTop(){
std::cout<<"Table top is being created ...\n";
sleep_for(seconds(7));
std::cout<<" Top done!\n";
}
auto MakeLegs(){
std::cout<<"Table legs are being created ...\n";
sleep_for(seconds(5));
std::cout<<" Legs done!\n";
}
int main(){
std::cout<<"Boss gets a new coffee table order.\n";
std::cout<<"Boss divides the tasks:\n";
std::thread carpenter1(MakeTop);
std::thread carpenter2(MakeLegs);
std::cout<<"Boss is waiting for carpenters...\n";
carpenter1.join();
carpenter2.join();
std::cout<<"Boss attaches top and legs of coffee table.";
unsigned int n = std::thread::hardware_concurrency();
std::cout << n << " concurrent threads are supported.\n";
return 0;
}
Latest Posts
- A C++ MPI code for 2D unstructured halo exchange
- Essential bash customizations: prompt, ls, aliases, and history date
- Script to copy a directory path in memory in Bash terminal
- What is the difference between .bashrc, .bash_profile, and .profile?
- A C++ MPI code for 2D arbitrary-thickness halo exchange with sparse blocks