In a multithreaded environment, concurrent read and write operations on shared data (global variables, heap memory, static resources, etc.) can lead to the following issues:
Data Race resulting in inconsistent logical values after value modification: When multiple threads modify the same data simultaneously, due to non-atomic operations, the final result may be unpredictable.For example, if two threads each execute x++ on the same variable int x=0 for 10,000 times, the expected result should be 20,000, but the actual result may be much lower than this value.;
Threads may read outdated data due to CPU caching and compiler optimizations (such as instruction reordering). For example: after thread A modifies data, it may not be synchronized to main memory in time, and thread B still reads the old value.;
Data inconsistency not only leads to logical errors but may also cause the program to crash.For example, multiple threads simultaneously inserting or deleting from a linked list.
-
1. Atomic Typesatomic
std::atomic is a template class introduced in C++11 that provides atomic operations without the need for explicit mutexes. It is suitable for basic data types (such as integers and pointers) and trivially copyable types. Atomic operations ensure indivisibility, avoid data races, and guarantee memory order (the default is the strictest memory order std::memory_order_seq_cst).Lock-free concurrency can achieve performance over 10 times higher than mutex.
#include <atomic>
#include <thread>
#include <vector>
std::atomic<int> counter(0); // Atomic integer
void increment(int n) {
for (int i = 0; i < n; ++i) {
counter.fetch_add(1, std::memory_order_relaxed); // Atomic increment
}
}
int main() {
std::vector<std::thread> threads;
for (int i = 0; i < 10; ++i) {
threads.emplace_back(increment, 1000);
}
for (auto&& t : threads) t.join();
std::cout << counter; // Always outputs 10000
}
-
2. Mutexes (Locks)
std::mutex is the most basic mutex in C++11, providing exclusive ownership characteristics—i.e., it does not support recursive locking on std::mutex objects..
/*-------std::mutex usage example-------*/
#include <mutex>
#include <thread>
std::mutex mtx;
int shared_data = 0;
void unsafe_increment() {
mtx.lock(); // Block and wait for lock
++shared_data; // Critical section operation
mtx.unlock(); // Must explicitly release
}
// Recommended way
void safe_increment2() {
std::lock_guard<std::mutex> lock(mtx); // Lock on construction, automatically unlock on destruction
++shared_data;
// Guarantees unlock even if an exception is thrown
}
// Performance optimization method
void safe_increment3() {
// Non-critical section operations...
{
std::lock_guard<std::mutex> lock(mtx);
// Minimize critical section
}
// Other operations...
}
std::shared_mutex is a synchronization primitive introduced in C++17, used to manage access permissions for shared resources among multiple threads, allowing multiple read threads to access the resource simultaneously, but only allowing a single write thread to modify the resource. It supports shared locks (multiple read threads concurrently accessing) and exclusive locks (single write thread modifying the resource). In read-heavy scenarios, it can significantly improve efficiency compared to ordinary mutexes (such as std::mutex). It works in conjunction with std::shared_lock (shared lock) and std::unique_lock (exclusive lock) for flexible lock management.
/*-------std::shared_mutex usage example-------*/
#include <iostream>
#include <shared_mutex>
#include <vector>
std::shared_mutex rw_mutex;
std::vector<int> data;
// Write thread (exclusive access)
void writer() {
std::unique_lock<std::shared_mutex> lock(rw_mutex); // Acquire write lock
data.push_back(rand()); // Modify data
}
// Read thread (shared access)
void reader() {
std::shared_lock<std::shared_mutex> lock(rw_mutex); // Acquire read lock
for (int n : data) { // Safe read
std::cout << n << " ";
}
}
-
3. Deadlocks
Deadlock refers to a phenomenon where multiple processes are unable to proceed due to waiting for resources or communication. Four necessary conditions must be met for a deadlock to occur: mutual exclusion (resource exclusivity), hold and wait (holding resources while requesting new ones), no preemption (resources cannot be forcibly taken), and circular wait (forming a wait chain).
Avoidance Methods
-
Break the hold and wait condition
Request all resources at once: Processes should request all required resources at once before starting, avoiding repeated requests during execution.Release resources in stages: Allow processes to gradually release already held resources before requesting new ones.
-
Break the no preemption condition
Force resource preemption: When resources are occupied, other processes can forcibly acquire the resources.
-
Break the circular wait condition
Orderly resource allocation: Specify the order of resource requests to break the circular chain.
The Banker’s Algorithm: Dynamically detect resource allocation states to avoid over-allocation.
-
Other Strategies
Timeout mechanism: Set a timeout period; if not completed within the timeout, release resources.
Reduce lock granularity: Decrease the coverage of locks to avoid excessive processes depending on the same resource.
/*-------Deadlock example-------*/
#include <mutex>
std::mutex mtxA, mtxB;
// Typical AB-BA deadlock
void thread1() { // Thread 1: lock A first, then lock B
std::lock_guard<std::mutex> lockA(mtxA);
std::lock_guard<std::mutex> lockB(mtxB); // Blocks if thread 2 has locked B
}
void thread2() { // Thread 2: lock B first, then lock A
std::lock_guard<std::mutex> lockB(mtxB);
std::lock_guard<std::mutex> lockA(mtxA); // Blocks if thread 1 has locked A
}
-
4. Condition Variables (condition_variable)
Condition variables (condition_variable) are a synchronization mechanism introduced in the C++11 standard for precise cooperation between threads in a multithreaded environment, triggering thread wake-up through changes in shared variable states.Condition variables must be used in conjunction with mutexes (mutex) to ensure thread safety when modifying shared variables. The core mechanisms include:
-
Waiting (wait): Threads block and wait for the condition to be met.
-
Notification (notify_one/notify_all): Wake up one or all waiting threads.
-
Predicate Check: Threads must check conditions through predicates (such as lambda expressions) to determine if conditions are met.
/*-------Typical usage scenario of condition variables: Producer-Consumer model-------*/
#include <mutex>
#include <queue>
std::queue<int> data_queue;
std::mutex mtx;
std::condition_variable cv;
// Producer
void producer() {
std::unique_lock<std::mutex> lock(mtx);
data_queue.push(42);
cv.notify_one(); // Wake up one consumer
}
// Consumer
void consumer() {
std::unique_lock<std::mutex> lock(mtx);
cv.wait(lock, [] { return !data_queue.empty(); }); // Automatically release lock if condition not met
int data = data_queue.front();
data_queue.pop();
}
-
Conclusion