In C++, the concept of multithreading is a robust approach wherein a software application is divided into smaller units of operation referred to as threads. Multithreading empowers a CPU or a single core within a multi-core processor to execute numerous threads simultaneously. Utilizing C++ for programming permits software to assign tasks among separate threads that have the ability to function autonomously or interact with one another.
In today's computing landscape, leveraging multiple CPU cores is crucial for developing responsive, effective, and scalable applications. C++ multithreading empowers programmers to attain this by enabling simultaneous execution of code segments.
What is the Thread?
In C++, a thread is a form of operational entity utilized within a specific operation. In a multitasking setting, numerous operations can operate simultaneously. Similarly, we can run the identical operation multiple times by employing threads. Each operation in this scenario is linked with a component known as a thread.
Create a Thread
The thread class is located within the std::thread namespace. Instantiating an object of this class initiates a new thread that runs the designated callable function.
Syntax
It has the following syntax:
thread name_of_thread(callable);
In this structure,
- nameofthread: It represents an instance of the thread class.
- Callable: It denotes a callable entity such as a function pointer or function object.
C++ Thread Example
Let's consider an example to demonstrate threading in C++.
Example
#include <iostream>
#include <thread> // Required for std::thread
using namespace std; //using standard namespace
void task() {
cout << "Thread is running.\n";
}
int main() { //main function
thread t(task); // Create a thread that runs the task function
t.join(); // Wait for the thread to finish
return 0;
}
Output:
Thread is running.
Explanation
In this instance, the thread represents the class responsible for thread creation and management. Following this, the .join function guarantees that the primary thread halts its execution until the newly created thread completes its task.
Important Header Files using Thread
There are multiple header files essential for implementing multithreading in C++. Here are a few of them:
#include <thread> // For std::thread
#include <mutex> // For std::mutex, std::lock_guard
#include <future> // For std::async, std::future
#include <atomic> // For atomic operations
#include <chrono> // For time-related functions
Defining the Callable
In the realm of C++, a callable denotes any function or object capable of being executed by a thread. This category encompasses various entities like function pointers, lambda expressions, and objects that redefine their execution behavior by overloading the operator.
In C++, a callable can be declared in four different manners. These include:
1) Using the function object:
In the thread object, a function object can be employed as a callable entity. To achieve this functionality, it is necessary to define a class for the function object and then overload the operator. Upon creating the thread, the code within the overloaded function is executed.
Syntax
It has the following syntax:
std::thread thread_object(functObject_class (), params)
C++ Thread Using Function Object Example
Let's consider an example to demonstrate multithreading with function objects in C++.
Example
#include <iostream>
#include <thread>
using namespace std; //using standard namespace
class Functor {
public:
void operator()(int x) {
cout << "Function Object Thread: Value = " << x << endl;
}
};
int main() { //Main Function
thread t(Functor(), 10); // Pass an instance of the class and argument
t.join(); // Wait for thread to finish
return 0;
}
Output:
Function Object Thread: Value = 10
Explanation
In this instance, we are outlining the Functor class, which makes use of overloading the operator. Subsequently, when the std::thread object constructor is called, it automatically invokes the execution of the operator method on a separate thread. The primary thread then remains in a waiting state for the secondary thread to finish by employing the t.join method.
2) Using Function Pointer
In C++, utilizing a function pointer is the most straightforward and direct approach to specifying a callable entity for a std::thread. It involves providing the memory address of a non-member function to the constructor of the thread.
Syntax
It has the following syntax:
void funct_call(params)
std::thread thread_obj(funct_call, params); //thread using function pointer
Once the function is defined, it is necessary to instantiate the thread object with the specified function that can be called. Arguments can be supplied to the function following the function name of the object.
C++ thread using Function Pointer Example
Let's consider a scenario to demonstrate threading with a function pointer in C++.
Example
#include <iostream>
#include <thread>
using namespace std; //using standard namespace
void functionPointerExample(int x) {
cout << "Function Pointer Thread: Value = " << x << endl;
}
int main() { //Main function
thread t(functionPointerExample, 20); // Pass function name and argument
t.join(); // Wait for thread to finish
return 0;
}
Output:
Function Pointer Thread: Value = 20
3) Using a Lambda Expression
In C++, a lambda function can be employed as a callable object while initializing a std::thread. Lambdas prove to be particularly handy for executing brief, inline operations without the necessity of declaring a distinct function.
Syntax
It has the following syntax:
int main()
{
std::thread t1(callable_code);
.....
t1.join();
.....
}
In the provided scenario, the primary function must pause execution until thread t1 completes its task. Typically, the join method of a thread prevents concurrent operations or functionalities until the invoking thread completes its processing.
C++ thread using Lambda Expression Example
Let's consider a scenario to demonstrate the concept of threading with lambda expressions in C++.
Example
#include <iostream>
#include <thread>
using namespace std; //using standard namespace
int main() { //Main Function
auto lambda = [](int x) {
cout << "Lambda Thread: Value = " << x << endl;
};
thread t(lambda, 30); // Lambda and its parameter
t.join(); // Wait for thread to finish
return 0;
}
Output:
Lambda Thread: Value = 30
Explanation
In this instance, we generate nameless functions using lambdas, which are situated directly within the code. When utilizing the std::thread constructor, it necessitates the lambda function along with its corresponding arguments.
4) Using Member Function
In the C++ programming language, we have the ability to employ a member function of a class as a callable object for std::thread. This feature enables us to spawn threads that can work on particular instances of objects.
Syntax
It has the following syntax:
std::thread thread_name(&ClassName::memberFunction, &object, args...);
C++ threads using Member Function Example
Let's consider an example to demonstrate threads using the member function in C++.
Example
#include <iostream>
#include <thread>
using namespace std; //using standard namespace
class Car {
public:
void run(int count) {
for (int i = 1; i < count; ++i) {
cout << "Car is Running: " << i << endl;
}
}
};
int main() { //Main Function
Car car;
thread t(&Car::run, &car, 6); // Pass address of member function and object
t.join();
return 0;
}
Output:
Car is Running: 1
Car is Running: 2
Car is Running: 3
Car is Running: 4
Car is Running: 5
Explanation
In this instance, a pointer to the member function run is obtained using a &Car::run. Subsequently, the object &car is utilized to invoke the member function. The argument 6 is then supplied to the run function. Ultimately, the t.join method guarantees that the main thread pauses to allow the child thread to complete its execution.
Example of Callable Thread
We provide a comprehensive code illustration demonstrating how to create and run a thread within the showcased program.
Example
#include <iostream>
#include <thread>
using namespace std; //using standard namespace
// function to be used in callable
void func_dummy(int N)
{
for (int i = 0; i < N; i++) {
cout << "Thread 1 :: callable => function pointer\n";
}
}
// A callable object
class thread_obj {
public:
void operator()(int n) {
for (int i = 0; i < n; i++)
cout << "Thread 2 :: callable => function object\n";
}
};
int main() //Main Function
{
// Define a Lambda Expression
auto f = [](int n) {
for (int i = 0; i < n; i++)
cout << "Thread 3 :: callable => lambda expression\n";
};
//launch thread using function pointer as callable
thread th1(func_dummy, 2);
// launch thread using function object as callable
thread th2(thread_obj(), 2);
//launch thread using lambda expression as callable
thread th3(f, 2);
// Wait for thread t1 to finish
th1.join();
// Wait for thread t2 to finish
th2.join();
// Wait for thread t3 to finish
th3.join();
return 0;
}
Output:
Thread 1 :: callable => function pointer
Thread 3 :: callable => lambda expression
Thread 3 :: callable => lambda expression
Thread 2 :: callable => function object
Thread 2 :: callable => function object
Thread 1 :: callable => function pointer
Explanation
In this instance, we have generated three threads employing three distinct callables: function pointer, object, and lambda expression. We instantiate two of each thread and initiate their execution. As depicted in the result, the three threads function concurrently and autonomously.
Thread Management in C++
In C++, managing threads involves overseeing and coordinating threads generated through the creation of objects from the std::thread class. Effective thread management involves supervising execution, implementing synchronization techniques, handling termination procedures, and ensuring proper resource cleanup to avoid issues like data races, memory leaks, and undefined behavior.
There are multiple functions available for controlling threads, which can be utilized for carrying out various tasks simultaneously.
| Function | Description |
|---|---|
| join() | It waits for the thread to finish execution. |
| detach() | It is used to detache the thread to run independently (daemon-like). |
| joinable() | Check if the thread can be joined or detached. |
| get_id() | It returns the thread's unique identifier. |
| native_handle() | It returns the native handle for platform-specific operations. |
| swap(thread& other) | It swaps the thread handles between two thread objects. |
| hardware_concurrency() (static) | It returns the number of threads supported by the system (concurrent threads). |
| atomic | It can be used to manage shared variables between threads in a thread-safe manner without using locks. |
C++ Thread Management Example
Let's consider an example to demonstrate the various roles of threads in C++.
Example
#include <iostream>
#include <thread>
#include <chrono>
using namespace std; //using standard namespace
void task(int n) {
cout << "Thread " << n << " started. ID: " << this_thread::get_id() << endl;
this_thread::sleep_for(chrono::milliseconds(300));
cout << "Thread " << n << " finished.\n";
}
int main() { //Main Function
cout << "System supports " << thread::hardware_concurrency() << " concurrent threads.\n\n";
thread t1(task, 1);
thread t2(task, 2);
thread t3(task, 3);
thread t4(task, 4); // Detached
cout << "Thread IDs:\n";
cout << "t1: " << t1.get_id() << "\n";
cout << "t2: " << t2.get_id() << "\n";
cout << "t3: " << t3.get_id() << "\n";
cout << "t4: " << t4.get_id() << "\n\n";
if (t1.joinable()) t1.join();
if (t2.joinable()) t2.join();
if (t3.joinable()) t3.join();
if (t4.joinable()) t4.detach();
cout << "\nMain thread ends after managing all threads.\n";
this_thread::sleep_for(chrono::seconds(1)); // Let detached t4 finish
return 0;
}
Output:
System supports 4 concurrent threads.
Thread 2 started. ID: 131265380484800
Thread 3 started. ID: 131265369999040
Thread 1 started. ID: 131265390970560
Thread IDs:
t1: 131265390970560
t2: 131265380484800
t3: 131265369999040
t4: 131265359513280
Thread 4 started. ID: 131265359513280
Thread 3 finished.
Thread 4 finished.
Thread Thread 2 finished.
1 finished.
Main thread ends after managing all threads.
Explanation
In this instance, we illustrate the management of C++ threads by utilizing functionalities from std::thread. The code initiates the creation of four threads, with three threads being joined and one thread left to execute independently. The result displays the identification of each thread in addition to providing information about the operating system for concurrent tasks.
Benefits of Multithreading
Several benefits of the multithreading in C++ are as follows:
- It helps to enhance performance on multi-core systems.
- It offers better responsiveness in GUI applications.
- It has an efficient resource utilization.
- It has parallel execution of independent tasks.
Context Switching in Multithreading
CPU executions perform context switching to transition between various threads. The operating system is responsible for saving the current thread's state and loading the state of the next thread to facilitate concurrent thread operations. The overhead of context switching in multitasking scenarios can significantly affect execution speed, especially when frequent context changes occur.
C++ Context Switching in Multithreading Example
Let's consider an example to demonstrate the concept of context switching in multithreading within C++.
Example
#include <iostream>
#include <thread>
#include <chrono>
using namespace std; //using standard namespace
void task1() {
for (int i = 0; i < 1000000; ++i) {
// Simulate work
}
}
void task2() {
for (int i = 0; i < 1000000; ++i) {
// Simulate work
}
}
int main() { //Main Function
auto start = std::chrono::high_resolution_clock::now();
std::thread t1(task1);
std::thread t2(task2);
t1.join();
t2.join();
auto end = std::chrono::high_resolution_clock::now();
std::chrono::duration<double> duration = end - start;
std::cout << "Total time taken: " << duration.count() << " seconds\n";
return 0;
}
Output:
Total time taken: 0.0120467 seconds
Explanation
In this instance, we determine the running time of two tasks that are executed concurrently utilizing threads. Subsequently, we utilize std::chrono to capture the beginning and ending times, create two threads to run task1 and task2 concurrently, and then synchronize them before computing and displaying the overall duration.
Passing Arguments to threads in C++
In C++ threading, we have the option to transmit multiple parameters by leveraging the s structure within a std::thread constructor. This enables us to duplicate or transfer the parameter into the internal memory of the thread entity before forwarding it to the callable function.
Syntax
It has the following syntax:
std::thread thread_name(callable, arg1, arg2, ...);
C++ Passing Arguments to threads Example
Let's consider an example to demonstrate how arguments are passed to threads in C++.
Example
#include <iostream>
#include <thread>
using namespace std; //using standard namespace
void show_msg(string msg, int count) {
for (int i = 0; i < count; ++i) {
cout << msg << " " << i << endl;
}
}
int main() { //main function
thread t(show_msg, "Hello! this is from thread", 3); //It prints the output
t.join();
return 0;
}
Output:
Hello! this is from thread 0
Hello! this is from thread 1
Hello! this is from thread 2
Explanation
In this instance, we initiate a new thread to run the display_alert function, displaying a message thrice. Following that, the t.join method guarantees that the primary thread pauses to allow the new thread to complete its tasks before concluding.
Problems with Multithreading
Numerous issues associated with multithreading in C++ include:
1) Race Conditions
In C++, a race condition arises when multiple threads concurrently access shared data depending on the timing of their operations. In the absence of appropriate synchronization, this can result in data corruption and inconsistent outcomes.
2) Deadlocks
In C++, a deadlock arises when numerous threads get trapped in a scenario where they are waiting for resources from other threads while also preventing each other from progressing, leading to a standstill. This situation typically happens because threads acquire locks on resources in a way that creates a circular dependency.
3) Livelocks
In the C++ programming language, livelock arises when active threads are caught in a perpetual cycle while trying to resolve resource conflicts, distinct from deadlocks.
4) Starvation
Thread starvation occurs when threads require resources for extended periods, while other threads continuously acquire and hold onto these resources. This issue can lead to a deadlock situation with lower-priority threads getting indefinitely blocked due to the priority-based scheduling mechanism.
C++ Multithreading MCQs
1) What happens if std::thread is created but not joined or detached before it is destroyed in C++?
- It is automatically joined
- The program continues normally
- The program execution will produce an exception that terminates the running code.
- The thread automatically detaches upon destruction.
c) The execution of the program will result in an exception that will halt the running code.
2) Which of the following statements is accurate about the std::async function in C++?
- The std::thread functionality produces an independent thread when executed.
- It defers execution until get is called.
- It returns void.
- It blocks the calling thread until completion.
Option b) Delays execution until the get method is invoked.
3) What is the main purpose of std::mutex in C++ multithreading?
- To pause thread execution
- To create new threads
- To lock shared resources for thread safety
- To terminate threads
To secure shared resources for ensuring thread safety.
4) Which one of the following options is the main drawback of using detached threads in C++?
- Joined threads execute at a faster speed than detached threads do.
- The parent thread maintains no access to their results.
- They consume more memory
- All detached threads need static declaration.
b) The main thread does not have access to its outcomes.
5) Which of the following is NOT a valid way to create a thread in C++ using std::thread?
- Passing a function pointer
- Passing a lambda function
- Passing a functor (function object)
- Calling run on a thread object