DEV Community

Cover image for Multithreading | How to Create and Manage Threads in C++
Shreyos Ghosh
Shreyos Ghosh

Posted on • Updated on

Multithreading | How to Create and Manage Threads in C++

In any language, threads can be a very useful tool to do a certain task more efficiently by allowing the program to do multiple operations (e.g. input, processing, storage and output) at the same time. Threads can be utilized to allocate and execute different parts of a program without interrupting the main program.

Here, I will briefly talk about concurrency and parallelism as well as how you can implement multithreading in your C++ program.

What's a Thread?

A thread is a sequence of instructions that run in parallel to other threads within a process.

A process is a program which is under execution i.e. when a program is loaded into the memory then it becomes a process.

Now, that we know about threads, let's see the differences between concurrency and parallelism

Concurrency vs. Parallelism

Concurrency is closely related to multithreading, where more than one or many threads are handled at a period of time by switching the control from one thread to another back and forth. It may seem like all the threads are being executed at the same time. As this whole process is very quick but in reality, only one thread can be handled at a time. In concurrency, multiple threads can communicate with each other.

Parallelism is achieved when multiple threads are handled parallel to each other in hardware resources (multi-core, multi-chip or many cores).

Let's explain these two terms with a real-life example. Suppose there is one ticket counter managing two queues. Where only one person is getting a ticket at a time but that person can be from any queue. Here, if one queue is getting served then the other queue has to wait for it's turn and vice versa. This is an example of concurrency.

Diagram of Concurrency

Now, if we increase the number of counters and make it two, then each queue gets it's own ticket counter and they can be managed in a parallel manner.

Diagram of Parallelism

Here, as we can see each counter can only handle one customer or queue at a time. Similarly, consider these queues as a thread and counters as a core of a CPU with each core having the ability to handle only one thread at once, that's what we call a single-threaded core.

Now if you have two threads to run in a single-threaded core, then you need to run them in a concurrent manner(i.e. having two queues with only one counter).

But when you have more than one single-threaded core then you have the ability to run those two threads parallelly(i.e. having two queues with two counters/one counter each).

Concurrency --> Illusion
Parallelism --> True Concurrency

Nowadays, we can actually run two threads in one core at once (you can visualize them as a bigger core which is logically divided into two sub-core/smaller-core) which is also termed as hyperthreading.

For those who have confusion with the term "thread" being used by those CPU manufacturing companies vs. in software. What these companies mean by saying their CPU has "X" amount of threads, is actually how many threads that the CPU can run parallelly. But this doesn't mean that your CPU is limited to run only "X" amount of threads. Obviously, your CPU can run thousands of threads concurrently, but in that case, some threads will have to share hardware resources.

Managing Threads:

Up to this point, we've gained a little bit of understanding about multithreading. So let's have a look at how we can launch/create multiple threads in our C++ code.

Create a Thread:

There are three possible ways that we can create a thread. Which are,

  1. Using Function Pointer

  2. Using Function Object

  3. Using Lambda Expression

Note: To work with threads, we will have to use the C++ standard library header <thread>. Every C++ program has at least one thread on which main() is called at program startup, it is called a main thread.

Using Function Pointer:

Here, we will pass a function pointer as a task for the thread object of std::thread class.

1. void my_task(parameters);  //callable function
2. std::thread my_worker(my_task, arguments);  //construction of
   //thread object specifying the task.
Enter fullscreen mode Exit fullscreen mode

Code Example:

#include <iostream>
#include <thread>

void my_task(std::string name) {
    std::cout << "Hello world of " << name << "\n";
}
int main() {
    std::thread my_worker(my_task, "thread!");
    my_worker.join();
    return 0;
}
Enter fullscreen mode Exit fullscreen mode

Explanation:

As I said, we are passing a function pointer my_task as a task to the thread object my_worker and as soon as the construction of the thread object is done, the thread starts the execution of the said task.

In total there are two threads present in the above program, one is the main thread and another one is the worker thread.

Notice, that here I've used thread join method join(), which we'll talk about later.

Using Function Object:

Function object which is also called as a functor, can be used to pass a callable object/instance of a class as a task to the thread object by overloading the function call operator () using operator function.

object + () = functor/function object

1. class my_functor {  //function object class
   public:
       void operator()(parameters) {  //operator function 
           //task list                //overloading () operator
       }
   };
2. my_functor Functor;  //declaration of object
3. std::thread my_worker(Functor, arguments);  //construction
   //of thread object using functor
Enter fullscreen mode Exit fullscreen mode

Code Example:

#include <iostream>
#include <thread>

class my_functor {
public:
    //operator overloading
    void operator()(std::string name) {
        std::cout << "Hello it's me, " << name;
    }
};
int main(){
    my_functor Functor;
    std::thread my_worker(Functor, "Functor!");
    my_worker.join();
    return 0;
}
Enter fullscreen mode Exit fullscreen mode

Explanation:

Here, we are passing a function object Functor() which belongs to the function object class my_functor as a task to the thread object my_worker, by overloading the function call operator/parenthesis operator ().

Using Lambda Expression:

A lambda Expression, which consists of a nameless function, can be passed as a task (i.e. a callable type) to the thread object.

1. auto my_lambda_exp = [capture clause](parameters) {
         //task list                    //lambda expression
   };
2. std::thread my_worker (my_lambda_exp, arguments);
   //construction of thread object using lambda expression
Enter fullscreen mode Exit fullscreen mode

Code Example:

#include <iostream>
#include <thread>

auto my_lambda_exp = [](std::string name){
    std::cout << "Hello " << name;
};

int main() {
    std::thread my_worker (my_lambda_exp, "World!");
    my_worker.join();
    return 0;
}
Enter fullscreen mode Exit fullscreen mode

Explanation:

Here, we are using a variable my_lambda_exp and passing it as a callable type for the thread object my_worker .
The lambda expression,
[](std::string name){std::cout << "Hello " << name;}
which is assigned to the variable my_lambda_exp, can also be used as a task for the thread object.

Code Snippet:

std::thread my_worker ([](std::string name){
    std::cout << "Hello " << name;
}, "World!");
Enter fullscreen mode Exit fullscreen mode

For this case, you don't need to assign the lambda expression to a variable. You can put it directly and it will be treated as a callable type. Both of the given examples will work the same.

Now, it's time to talk about that join() method that we previously used.

Join and Detach a Thread:

When you launch a thread object, as said the thread will start the process of execution. At this point, you have the main thread and a worker thread running.

Now, you have two options either you can use join() i.e. pause the main thread and wait for the worker thread to complete the task and then continue the main thread execution or you can use detach() and continue the main thread execution and forget about the worker thread.

It is mandatory to either join or detach a thread before the main program execution is completed or it will be considered as an error and the std::terminate will be called by the thread destructor std::thread::~thread for garbage collection.

Joining a thread:

Let me give you a code example,

1. void my_work(parameters);  //callable function
2. std::thread my_worker(my_work, arguments);  //construction of
   //thread object passing the task
3. my_worker.join();  //joining the worker thread
Enter fullscreen mode Exit fullscreen mode

Code Example:

#include <iostream>
#include <thread>

void my_work(int count) {
    while(count++ < 2){
        std::cout << "Worker thread is working..." << "\n";
    }
    std::cout << "The task is completed!" << "\n";
}
int main() {
    std::cout << "This is main thread!" << "\n";
    std::thread my_worker(my_work, 0);
    my_worker.join();
    std::cout << "Main thread execution completed!" << "\n";
    return 0;
}
Enter fullscreen mode Exit fullscreen mode

Code Output:

 This is main thread!
 Worker thread is working...
 Worker thread is working...
 The task is completed!
 Main thread execution completed!
Enter fullscreen mode Exit fullscreen mode

Explanation:

Thread Joining

In the above program, after passing the function pointer my_work to the thread object my_worker, we've called the join() method and because of this, the main thread execution will be paused until the given task for the worker thread is done.

Before talking about the detach method, let's understand

What is joinable()?

std::thread::joinable is a method used to check if a thread has been joined/detached or not.

By default, every constructed thread object is joinable, Which means joinable() == true.

If not changed as usual this will trigger an error and the destructor std::thread::~thread will terminate the program by the end of the main program execution. Once a thread object is joined/detached joinable() == false.

Detaching a thread:

Let's say you're in such a scenario, where you don't want to pause the execution of the main program or the task for the worker thread is going to take a lot of time that you can't afford to wait on. That's when you can use detach() to run your thread in the background.

By using detach method we can execute the main thread and the worker thread (i.e. the caller thread and the called thread) independently from each other. And as soon as either one's execution ends, the thread destructor std::thread::~thread will be called and the resources will be released.

1. void my_work(parameters);  //callable function
2. std::thread my_worker(my_work, arguments);  //construction of
   //thread object passing the task
3. my_worker.detach();  //detaching the worker thread
Enter fullscreen mode Exit fullscreen mode

Code Example:

#include <iostream>
#include <chrono>
#include <thread>
using namespace std::this_thread;
using namespace std::chrono;

void my_work(int count) {
    while(count++ < 2){
        std::cout << "Worker thread is working..." << "\n";
    }
    std::cout << "The task is completed!" << "\n";
}
int main() {
    std::cout << "This is main thread!" << "\n";
    std::thread my_worker(my_work, 0);
    my_worker.detach();
    std::cout << "Main thread execution completed!" << "\n";
    //sleep_until(system_clock::now() + seconds(2));
    return 0;
}
Enter fullscreen mode Exit fullscreen mode

Code Output:

This is main thread!
Main thread execution completed!
Enter fullscreen mode Exit fullscreen mode

Explanation:

Thread Detaching

If you run the given example code, you'll probably get an output like that.

So, let's talk about why this is happening...

I previously said that as soon as the construction of the thread object(of the worker thread) is done, the thread(worker thread) starts the execution of the said task.

Now there is a little more to be told! Let me explain...

When we construct a thread object and pass a callable type to it, first the OS collects the request and only then it initiates the execution. As it sounds this whole operation is not instantaneous.

Now when you're using the detach method, that means the underlying thread will be detached from the thread object and main thread won't be paused rather it'll continue to run until the flow of the program reaches the end. Which is much quicker in this case. That's why we are not getting the desired output from the worker thread.

But if you undo the commented line in the above code, which will let the worker thread to complete it's task by putting the main program into sleep for 2 seconds, then you'll get this output

Code Output:

This is main thread!
Main thread execution completed!
Worker thread is working...
Worker thread is working...
The task is completed!
Enter fullscreen mode Exit fullscreen mode

Funfact: Threads might not be as evil as you thought!

Top comments (0)