How to write C++ concurrent code with std::thread

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;
}
(.Get 1)

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.

(.Get 1)

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);
(.Get 1)

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.
(.Get 1)

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;
}
(.Get 1)

Subscribe

I notify you of my new posts

Latest Posts

Comments

5 comments
keo 27-Mar-2023
Thanks a lot! This was very well explained and helpful
Prajakta 14-Jun-2023
Can you please explain thread synchronition method with classes
anonymous 3-Nov-2023
Very well explained and nice illustration. Possibly expand on newbie 'gotchas' on using threads. Overall well demonstrated, I noticed the function definition is a C++20 feature, its better to have return type made explicit, rather than auto. As this demonstrating C++ to new coders stick to standards for basic function definitions. Other than that very well done :-)
anonymous 3-Nov-2023
here is a suggestion of the function with return type: /* int wait_for_n_seconds(int sec) { std::cout << "Task started ...\n"; std::cout << "inside wait_for_"<< sec << "_seconds...\n"; std::this_thread::sleep_for(std::chrono::seconds(sec)); std::cout << " Task done!\n"; return 0; } */
AC 30-Mar-2024
Thanks it was helpful and simple explained.