Multithreading in C++ is a process that allows a C++ program to execute multiple threads at the same time. It helps to improve the performance and parallel computations of the applications. It was introduced with C++11 to help developers create and manage threads easily. In this article, we will discuss what multithreading is, what a thread is, creating a thread, benefits of multithreading, thread lifecycle, problems with multithreading, applications of multithreading, and best practices in C++.
Table of Contents:
What is Multithreading in C++?
Multithreading in C++ is referred to as the ability of a program to execute multiple threads or parts of code at the same time or concurrently. It helps to improve the performance on multi-core processors and keep the applications responsive. Also, it leads to some problems such as deadlock, race conditions, and complex debugging.
With C++11, built-in support for multithreading through libraries such as <thread>, <mutex>, etc., were introduced.
Key Benefits of Multithreading in C++
- Better Performance: It helps a program to use multiple cores, which leads to faster execution.
- Efficient Resource Utilization: Threads share the same memory space as a process, which reduces the memory overhead.
- Scalability: Multithreading helps applications to scale better on multiple multi-core platforms.
- Asynchronous Execution: It allows various programs to perform background tasks, such as file downloading, without blocking the main process.
- Improves Responsiveness: In applications such as GUIs or games, it keeps the program responsive.
What is a Thread?
A thread is the smallest execution unit in a program. When a program runs, the operating system gives it a process in which there can be one or more threads, each of which does different tasks at the same time.
There are two types of programs such as single-threaded and multi-threaded:
- Single-threaded Program: In a single-threaded program, only one task is done at a time.
- Multi-threaded Program: In a multi-threaded program, various tasks can be done together at the same time.
Creating a Thread in C++
A thread can be created using the std::thread class from the <thread> header. To start a thread, there is only a need to create a std::thread object and pass a callable such as a function or a functor to the constructor of the object. This process will immediately start the new thread and run the provided callables in parallel with the main thread.
Syntax:
std::thread thread_object(callable);
Here,
- std::thread – It is the class representing a thread of the C++ Standard Library.
- thread_object – It is the name of the thread that is created.
- callable – It is any callable object, such as a function, a lambda expression, or a functor.
Example:
Output:
The code shows how a thread is created that runs the sayHello function, prints “Hello from the thread!”, and then waits for the thread to finish using t.join() before printing “Hello from the main function!” in the main thread.
Defining the Callable in C++
A callable is a function or an object in C++ that is executed when the thread starts. It is passed to a thread constructor. It can be a function pointer, a lambda expression, a functor, a non-static or a static member function.
Let’s discuss each of the callables in brief with examples in C++.
1. Function Pointer
A function pointer is a pointer that points to a function. It can be used to refer to a function and pass that function to a thread for execution. There is only a need to define a normal function, pass the function name as a pointer to std::thread, and then the function is executed in a new thread when the thread starts.
Example:
Output:
The code shows how the std::thread t(sayHello) is used to create a thread that calls the sayHello function, t.join() makes sure that the main thread waits for t to finish, and then the output is printed to the console.
2. Lambda Expression
A lambda expression is an anonymous function that is defined inline within the code. It can be defined directly where it is called, and this makes it very flexible. The lambda can capture variables from its surrounding scope, can be passed directly into std::thread, and will be executed by the new thread.
Example:
Output:
The code shows how a thread is created using a lambda expression that prints “Hello from the lambda thread!” and also waits for the thread to finish executing using the t.join().
3. Function Object (Functor)
A functor is an object that is callable like a function. Functors can have state, which makes them useful when you need an object with some behavior or internal data. Define a class (or struct) that overloads the operator(), create an instance of this class, and pass it to std::thread. The object is then executed by the thread as if it were a function.
Example:
Output:
The code shows how a thread is created by passing a functor object, MyTask, that defines the work to be done, and waits for the thread to complete using t.join().
4. Non-Static Member Function
A non-static member function belongs to a specific instance of a class. It needs an object to be called. Thus, to run a non-static member function in a thread, both the member function pointer and the object instance must be passed to std::thread.
Example:
Output:
The code shows how a thread is created, which is used to run a non-static member function, sayHello, on an object obj, and also waits for the thread to complete using t.join(), then the result is printed to the console.
5. Static Member Function
A static member function is a function that belongs to the class itself rather than any object of the class, so it can be directly passed to std::thread without the need for any object instance.
Example:
Output:
The code shows how a thread is created, which is used to run the static member function sayHello directly without any object instance, and then waits for the thread to finish using t.join(), and then the message is printed to the console.
Thread Lifecycle in C++
The thread lifecycle is the cycle of the different stages a thread goes through during a program execution, from its creation to termination.
Stages of a C++ Thread Lifecycle:
Stage |
Description |
New |
A thread object is created, but no thread of execution starts yet. |
Runnable |
When a thread starts, it’s ready to run or is already running. |
Running |
The thread is actively executing its assigned function. |
Blocked / Waiting |
A thread can pause, waiting for I/O, synchronization, or some event (like join()). |
Terminated / Dead |
A thread finishes execution or is stopped, and it cannot be restarted. |
Example:
Output:
The code shows how a thread is created to run the task function, waits for it to finish using t.join(), and then prints a message from the main thread after the thread lifecycle is complete.
Thread Management in C++
Thread management in C++ refers to controlling the execution of a thread. It involves creating threads, waiting for them to finish, letting them run independently, checking if they are joinable, and handling their IDs. In C++, threads are managed through the <thread> header. It controls the complete life cycle of threads.
Key Thread Management Functions:
Function |
Description |
join() |
Blocks the calling thread until the thread finishes execution. |
detach() |
Allows the thread to run independently, and resources are recovered when it finishes. |
joinable() |
Returns true if the thread can be joined or detached and prevents errors. |
get_id() |
Returns the unique ID of the thread. |
hardware_concurrency() |
Returns the number of concurrent threads the system can ideally support. |
Example:
Output:
The code shows how a thread is created to run the task function, which checks if the thread is joinable, waits for it using t.join(), and then prints a message from the main thread after the thread has finished.
Problems with Multithreading in C++
Here are a few common problems with multithreading in C++:
- Deadlocks: A deadlock occurs when two or more threads are waiting on each other to release resources, which results in threads that are stuck indefinitely.
- Thread Contention: It happens when multiple threads want the same resource, which can lead to performance bottlenecks.
- Race Conditions: A race condition occurs when the behavior of a thread depends on the timing and ordering of events. This leads to unpredictable behavior depending on the combination of timing and ordering.
- Thread Leaks: It occurs when a thread is not properly joined or detached, which results in threads that are unused but still consume resources.
- Thread Safety Issues: Non-thread-safe libraries and functions will create problems when they are accessed by multiple threads.
- Starvation: It occurs when threads cannot obtain resources because other threads are using them.
Thread Synchronization in C++
Thread synchronization is important for managing access to shared resources between multiple threads. It makes sure that threads do not interfere with each other when they access or modify shared data and avoids issues such as data races and race conditions.
Types of Synchronization in C++
1. Mutexes (Mutual Exclusion)
A mutex is a lock that is used to protect shared data from being accessed by multiple threads simultaneously. Only one thread can take and use the mutex at a time, which ensures exclusive access.
Example:
Output:
The code shows how the std::mutex and std::lock_guard are used to increment a shared resource counter in a multithreaded environment, which makes sure that only one thread can access and modify the counter at a time.
2. std::lock_guard and std::unique_lock
These are RAII-style locks that manage mutexes.
- std::lock_guard: It automatically locks the mutex upon creation and releases it when the lock goes out of scope.
- std::unique_lock: It is more flexible than std::lock_guard, allows for manual locking and unlocking, and can be used with std::condition_variable.
Example:
Output:
The code shows how the std::lock_guard and std::unique_lock are used to increment a shared variable counter, which automatically manages the locking and unlocking of a mutex to prevent race conditions, and then the final counter value is printed to the console.
3. Condition Variables
It is used to synchronize threads because it waits until the condition is satisfied. Condition variables are always used in combination with a mutex. It is useful when one thread has to wait for another thread to do a task.
Example:
Output:
The code shows how the std::condition_variable is used so that one thread waits until a condition ready == true is met, and another thread sets the condition and then notifies the thread which is waiting to continue.
4. Read-Write Locks
A read-write lock allows multiple threads to read shared data simultaneously but gives exclusive access for write operations. It is best to use when there are many readers and few writers.
Example:
Output:
The code shows how the std::shared_mutex is used in the C++ program, where read_data takes a shared lock allowing multiple readers, and write_data takes an exclusive lock to write safely, which ensures that there is proper read/write synchronization.
Terminating Threads in C++
Threads in C++ terminate automatically when their function is finished. The thread termination can be explicitly managed by using join() to wait for the thread or detach() and let it run independently. If a thread is not joined or detached before destruction, std::terminate() is called, which will cause the program to abort. Thus, always check that the thread is joinable before joining or detaching it.
Example:
Output:
The code shows how a thread is terminated properly by checking that the thread is joinable before calling join() to avoid the potential errors and then the result is printed to the console.
Joining and Detaching Threads in C++
- Joining (join()): join() blocks the calling or main thread until the target thread finishes execution. It ensures the thread completes before the program proceeds.
- Detaching (detach()): detach() lets the thread run independently. Once it is detached, it cannot be joined, and its resources are automatically cleaned up when it finishes.
Example:
Output:
The code shows how the t1.join() blocks the main thread until t1 completes its running, and t2.detach() separates the thread, which allows it to execute in the background without synchronization, and then prints to the console.
Context Switch in Multithreading in C++
When the CPU switches from one running thread to another is called as a context switch in multithreading. In the context switch, the system saves the state of the current thread and then loads the next thread’s state so it can continue execution in its normal order without problems.
Example:
Output:
The code shows how task1 and task2 run concurrently in separate threads, simulate work using sleep_for, and synchronize their completion using join() to ensure both finish before the program ends.
Passing Arguments to Threads in C++
Arguments in C++ are passed to a thread function simply by providing them after the function name when the std::thread object is created. The thread constructor automatically forwards the arguments to the callable.
Example:
Output:
The code shows how the arguments are passed to a thread function by value, where the string “Intellipaat” is passed to the greet function running in a separate thread.
Multiprocessing vs Multithreading in C++
Feature |
Multiprocessing |
Multithreading |
Definition |
It allows running multiple processes |
It allows running multiple threads inside one process at the same time |
Memory |
Each process has its own memory space |
Threads share the same memory space |
Communication |
Slower |
Faster, due to shared memory |
Overhead |
High |
Low |
Failure Impact |
One process crash does not affect other processes |
A thread crash can bring down the whole process |
Use Cases |
High CPU tasks, true parallelism |
I/O tasks, lightweight parallelism |
Real-World Applications for Multithreading in C++
- Multithreading is used in game development, which helps games to run faster.
- It allows a server to handle a large number of user requests at the same time.
- Multithreading also enhances speed by processing the data, as you can divide the tasks and work on them at once.
- It facilitates real-time monitoring and processing of data from sensors with minimal delay.
- Multithreading allows video and audio tasks to run independently of each other, so you can keep the application smooth.
- It also helps in real-time processing of trades and facilitates faster decisions.
- Training AI models is quicker because calculations can be spread and more parallel threads can run using the multiple cores in the CPU.
- Multithreading makes file compression faster because we can apply a process to every individual section of the file, simultaneously.
Multithreading Best Practices in C++
- You should always use thread pools to manage multiple threads and reduce the overhead of creating threads.
- In threads, you must use std::mutex, std::lock_guard, or std::unique_lock for safe access to shared resources to prevent concurrent access from other threads.
- You should avoid deadlocks by acquiring locks in a stable order, or, if acquiring multiple mutexes, you should use std::lock.
- You also must avoid busy waiting and should use condition variables for thread notification.
- You must use thread-safe libraries and ensure the shared data structures are properly synchronized.
- Join threads before program exit or detach for programs designed to run independently.
- You should prefer std::async and std::future for straightforward tasks over managing threads yourself.
Conclusion
Multithreading helps you to write efficient and concurrent programs by executing multiple tasks at the same time. C++11 provides the std::thread library and synchronization tools, which help in the safe multithreaded development. But it also gives some problems, such as deadlocks, race conditions, etc. So, by understanding how multithreading works, its lifecycle, applications, and best practices, you can write an efficient C++ program using multithreading.
Multithreading in C++ – FAQs
Q1. How can I create a thread in C++?
You can create a thread in C++ by using the std::thread t(function_name); syntax.
Q2. What does join() do?
The join() blocks the main thread until the joined thread finishes.
Q3. When should I use detach()?
You should use detach() when there is a need to run a thread independently in the background.
Q4. How should I protect shared data?
You can protect shared data by using std::mutex with std::lock_guard and std::unique_lock in C++ programs.
Q5. When should I use condition_variable?
You should use condition_variable to make threads wait until a certain condition is met.