Cpp Multithreading
A thread is a lightweight unit of execution within a program, allowing the program to perform multiple tasks simultaneously.
Multithreading is a special form of multitasking. Multitasking allows a computer to run two or more programs concurrently.
Generally, there are two types of multitasking: **process-based and thread-based**.
* Process-based multitasking is the concurrent execution of programs.
* Thread-based multitasking is the concurrent execution of different parts of the same program.
!(#)
C++ multithreading involves creating and managing multiple threads that execute concurrently within a single program.
C++ provides robust support for multithreading, especially since the C++11 standard and later, making multithreading programming simpler and safer through the `` standard library.
* * *
## Concept Explanation
### Thread
* A thread is a single sequential flow of control within a program. Multiple threads can run independently within the same process.
* Threads share the process's address space, file descriptors, heap, and global variables, but each thread has its own stack, registers, and program counter.
### Concurrency vs. Parallelism
* **Concurrency**: Multiple tasks are executed in an interleaved manner within time slices, giving the appearance of simultaneous execution.
* **Parallelism**: Multiple tasks are executed simultaneously on multiple processors or processor cores.
The C++11 standard and later provide multithreading support. Core components include:
* **`std::thread`**: Used to create and manage threads.
* **`std::mutex`**: Used for mutual exclusion between threads, preventing multiple threads from accessing shared resources simultaneously.
* **`std::lock_guard`** and **`std::unique_lock`**: Used to manage the acquisition and release of locks.
* **`std::condition_variable`**: Used for condition variables between threads, coordinating waiting and notification.
* **`std::future`** and **`std::promise`**: Used to implement value passing and task synchronization between threads.
* * *
## Creating Threads
C++ 11 introduced the new standard thread library `std::thread`. `std::thread` is declared in the `` header file, so you need to include the `` header to use `std::thread`.
**Note:** Some compilers previously used the C++ 11 compilation flag **-std=c++11**:
g++ -std=c++11 test.cpp
std::thread
#include std::thread thread_object(callable, args...);
* **`callable`**: A callable object, which can be a function pointer, function object, Lambda expression, etc.
* **`args...`**: The argument list passed to the `callable`.
### Using Function Pointers
Creating a thread using a function pointer is the most basic method:
## Example
#include
#include
void printMessage(int count){
for(int i =0; i < count;++i){
std::cout<<"Hello from thread (function pointer)!n";
}
}
int main(){
std::thread t1(printMessage, 5);// Create thread, pass function pointer and arguments
t1.join();// Wait for thread to complete
return 0;
}
After compiling with g++ -std=c++11, the execution output is:
Hello from thread (function pointer)!Hello from thread (function pointer)!Hello from thread (function pointer)!Hello from thread (function pointer)!Hello from thread (function pointer)!
### Using Function Objects
Creating a thread using a function object defined by the `operator()` method in a class:
## Example
#include
#include
class PrintTask {
public:
void operator()(int count)const{
for(int i =0; i < count;++i){
std::cout<<"Hello from thread (function object)!n";
}
}
};
int main(){
std::thread t2(PrintTask(), 5);// Create thread, pass function object and arguments
t2.join();// Wait for thread to complete
return 0;
}
After compiling with g++ -std=c++11, the execution output is:
Hello from thread (function object)!Hello from thread (function object)!Hello from thread (function object)!Hello from thread (function object)!Hello from thread (function object)!
### Using Lambda Expressions
Lambda expressions can be used to define the code to be executed by the thread inline:
## Example
#include
#include
int main(){
std::thread t3([](int count){
for(int i =0; i < count;++i){
std::cout<<"Hello from thread (lambda)!n";
}
}, 5);// Create thread, pass Lambda expression and arguments
t3.join();// Wait for thread to complete
return 0;
}
After compiling with g++ -std=c++11, the execution output is:
Hello from thread (lambda)!Hello from thread (lambda)!Hello from thread (lambda)!Hello from thread (lambda)!Hello from thread (lambda)!
* * *
## Thread Management
### join()
`join()` is used to wait for a thread to complete its execution. If you destroy the thread object without calling `join()` or `detach()`, it will cause the program to crash.
t.join();
### detach()
`detach()` separates the thread from the main thread, allowing it to run independently in the background. The main thread no longer waits for it.
t.detach();
* * *
## Passing Arguments to Threads
### Pass by Value
Arguments can be passed to a thread by value:
std::thread t(func, arg1, arg2);
### Pass by Reference
If you need to pass reference arguments, you must use `std::ref`:
## Example
#include
#include
void increment(int& x){
++x;
}
int main(){
int num =0;
std::thread t(increment, std::ref(num));// Use std::ref to pass a reference
t.join();
std::cout<<"Value after increment: "<< num << std::endl;
return 0;
}
* * *
## Comprehensive Example
The following is a complete example demonstrating how to create threads using the three methods mentioned above and manage them.
## Example
#include
#include
using namespace std;
// A simple function to serve as the thread entry point
void foo(int Z){
for(int i =0; i < Z; i++){
cout<<"Thread using function pointer as callable argumentn";
}
}
// Class definition for a callable object
class ThreadObj {
public:
void operator()(int x)const{
for(int i =0; i < x; i++){
cout<<"Thread using function object as callable argumentn";
}
}
};
int main(){
cout<<"Threads 1, 2, 3 run independently"<< endl;
// Create thread using function pointer
thread th1(foo, 3);
// Create thread using function object
thread th2(ThreadObj(), 3);
// Create thread using Lambda expression
thread th3([](int x){
for(int i =0; i < x; i++){
cout<<"Thread using lambda expression as callable argumentn";
}
}, 3);
// Wait for all threads to complete
th1.join();// Wait for thread th1 to complete
th2.join();// Wait for thread th2 to complete
th3.join();// Wait for thread th3 to complete
return 0;
}
Compile with C++ 11 flag **-std=c++11**:
g++ -std=c++11 test.cpp
The output of the above code may vary on different platforms or each time it runs, because the execution order of threads is determined by the operating system's scheduling algorithm. Multiple threads run concurrently, and the output may be interleaved, for example:
Threads 1, 2, 3 run independentlyThread using function pointer as callable argumentThread using function object as callable argumentThread using lambda expression as callable argumentThread using function pointer as callable argument...
* * *
## Thread Synchronization and Mutual Exclusion
In multithreading programming, thread synchronization and mutual exclusion are two very important concepts. They are used to control multiple threads' access to shared resources to avoid issues like data races and deadlocks.
### 1. Mutex
A mutex is a synchronization primitive used to prevent multiple threads from accessing a shared resource simultaneously. When a thread needs to access a shared resource, it must first lock the mutex. If the mutex is already locked by another thread, the requesting thread will be blocked until the mutex is unlocked.
`std::mutex`: Used to protect shared resources and prevent data races.
std::mutex mtx; mtx.lock(); // Lock the mutex// Access shared resource mtx.unlock(); // Unlock the mutex
`std::lock_guard` and `std::unique_lock`: Automatically manage lock acquisition and release.
std::lock_guard lock(mtx); // Automatically lock and unlock// Access shared resource
Example of using a mutex:
## Example
#include
std::mutex mtx;// Global mutex
void safeFunction(){
mtx.lock();// Request to lock the mutex
// Access or modify shared resource
mtx.unlock();// Release the mutex
}
int main(){
std::thread t1(safeFunction);
std::thread t2(safeFunction);
t1.join();
t2.join();
return 0;
}
### 2. Locks
C++ provides several lock types to simplify the use and management of mutexes.
Common lock types include:
* `std::lock_guard`: A scoped lock that automatically locks the mutex upon construction and unlocks it upon destruction.
* `std::unique_lock`: Similar to `std::lock_guard`, but provides more flexibility, such as the ability to transfer ownership and manually unlock.
Example of using locks:
## Example
#include
std::mutex mtx;
void safeFunctionWithLockGuard(){
std::lock_guard lk(mtx);
// Access or modify shared resource
}
void safeFunctionWithUniqueLock(){
std::unique_lock ul(mtx);
// Access or modify shared resource
// ul.unlock(); // Optional: manually unlock
// ...
}
### 3. Condition Variable
Condition variables are used for coordination between threads, allowing one or more threads to wait for a certain condition to occur. They are typically used in conjunction with mutexes to achieve synchronization between threads.
`std::condition_variable` is used to implement waiting and notification mechanisms between threads.
std::condition_variable cv; std::mutex mtx;bool ready = false; std::unique_lock lock(mtx); cv.wait(lock, []{ return ready; }); // Wait for the condition to be met// Execute after the condition is met
Example of using a condition variable:
## Example
#include
#include
std::mutex mtx;
std::condition_variable cv;
bool ready =false;
void workerThread(){
std::unique_lock lk(mtx);
cv.wait(lk, []{return ready;});// Wait for the condition
// Perform work when the condition is met
}
void mainThread(){
{
std::lock_guard lk(mtx);
// Prepare data
ready =true;
}// Unlock when leaving scope
cv.notify_one();// Notify one waiting thread
}
### 4. Atomic Operations
Atomic operations ensure that access to shared data is indivisible. In a multithreading environment, an atomic operation either completes entirely or does not execute at all, without an intermediate state.
Example of using atomic operations:
## Example
#include
#include
std::atomic count(0);
void increment(){
count.fetch_add(1, std::memory_order_relaxed);
}
int main(){
std::thread t1(increment);
std::thread t2(increment);
t1.join();
t2.join();
return count;// Should return 2
}
### 5. Thread Local Storage (TLS)
Thread Local Storage allows each thread to have its own copy of data. This can be achieved using the `thread_local` keyword, avoiding contention for shared resources.
Example of using Thread Local Storage:
## Example
#include
#include
thread_local int threadData =0;
void threadFunction(){
threadData =42;// Each thread has its own copy of threadData
std::cout<<"Thread data: "<< threadData << std::endl;
}
int main(){
std::thread t1(threadFunction);
std::thread t2(threadFunction);
t1.join();
t2.join();
return 0;
}
### 6. Deadlock and Avoidance Strategies
A deadlock occurs when multiple threads are waiting for each other to release resources, but none can proceed. Strategies to avoid deadlocks include:
* Always request resources in the same order.
* Use timeouts when attempting to acquire resources.
* Use deadlock detection algorithms.
* * *
## Inter-thread Communication
`std::future` and `std::promise`: Implement value passing between threads.
## Example
std::promise p;
std::future f = p.get_future();
std::thread t([&p]{
p.set_value(10);// Set value, triggering the future
});
int result = f.get();// Get the value
Message queues (based on `std::queue` and `std::mutex`) for simple inter-thread communication.
* * *
C++17 introduced the parallel algorithms library (``), where some algorithms support parallel execution, allowing you to leverage multi-core CPUs for performance improvement.
## Example
#include
#include
#include
std::vector vec ={1, 2, 3, 4, 5};
std::for_each(std::execution::par, vec.begin(), vec.end(), [](int&n){
n *=2;
});
## Parallel Algorithms in the C++ Standard Library
> For more examples, see:
>
>
> C++ Multithreading: [
>
>
> **C++ std::thread**: [
YouTip