In multi-threaded development, data synchronization is often encountered, and in many cases, using locks is a good choice. There are several commonly used locks in C++:
Mutex (std::mutex)
- This is the most basic lock. It is used to protect shared resources. At any time, at most, only one thread can acquire the lock to access the protected resources. When a thread acquires a mutex, other threads trying to acquire the lock will be blocked until the thread holding the lock releases it.
- For example, in a multithreaded program, if multiple threads need to access and modify the same global variable, a mutex can be used to ensure that only one thread can perform modification operations at the same time, avoiding the error results caused by data competition.
#include <iostream> #include <mutex> #include <thread> std::mutex m; int counter = 0; void increment() { (); counter++; std::cout << "Counter value in thread " << std::this_thread::get_id() << " is " << counter << std::endl; (); } int main() { std::thread t1(increment); std::thread t2(increment); (); (); return 0; }
Recursive mutex (std::recursive_mutex)
- A recursive mutex allows the same thread to acquire the lock multiple times. It internally records the number of times the lock is acquired. Each time the lock is acquired, the count is increased by 1. Each time the lock is released, the count is reduced by 1. When the count is 0, the lock is truly released and can be obtained by other threads.
- Suppose in a complex function call chain, function A calls function B, function B calls function A, and these functions all need to access the same protected resource. If you use a normal mutex, a deadlock occurs, and a recursive mutex can avoid this because it allows the same thread to acquire the lock multiple times.
#include <iostream> #include <mutex> #include <thread> std::recursive_mutex rm; void recursiveFunction(int count) { (); if (count > 0) { std::cout << "Recursive call with count = " << count << std::endl; recursiveFunction(count - 1); } (); } int main() { std::thread t(recursiveFunction, 3); (); return 0; }
Read and write lock (std::shared_mutex) only exists at C++17
- Read and write locks are mainly used to distinguish between read and write operations on shared resources. It has two acquisition modes: Shared mode (read mode) and Exclusive mode (write mode).
- Multiple threads can acquire read and write locks in shared mode at the same time, meaning they can read shared resources at the same time without interfering with each other. However, when a thread wants to acquire a read-write lock (to perform a write operation) in exclusive mode, other threads (whether it is a read operation or a write operation) cannot acquire the lock until the write operation is completed and the lock is released. This mechanism can improve the concurrency performance of the program in scenarios where there are a large number of read operations and a small number of write operations. For example, in a cache system, multiple threads may often read the cached data and will only write operations when the cached data needs to be updated, and using a read-write lock can handle this situation well.
#include <iostream> #include <shared_mutex> #include <thread> #include <vector> std::shared_mutex smtx; int shared_data = 0; void read_data() { std::shared_lock<std::shared_mutex> lock(smtx); std::cout << "Read data: " << shared_data << std::endl; } void write_data(int new_value) { std::unique_lock<std::shared_mutex> lock(smtx); shared_data = new_value; std::cout << "Wrote data: " << shared_data << std::endl; } int main() { std::vector<std::thread> read_threads; for (int i = 0; i < 5; i++) { read_threads.push_back(std::thread(read_data)); } std::thread write_thread(write_data, 10); for (auto& t : read_threads) { (); } write_thread.join(); return 0; }
Timed mutex (std::timed_mutex)
- The timing mutex is
std::mutex
extension. In addition to havingstd::mutex
In addition to the basic functionality, it also allows threads to set a timeout time when trying to acquire the lock. - If the lock cannot be acquired within the specified timeout time, the thread will not wait for it, but can perform other operations or return an error message. This is very useful in some time-sensitive scenarios. For example, in a real-time system, threads cannot block indefinitely because they are waiting for a lock, and need to give up the acquisition after a certain period of time and perform other processing.
#include <iostream> #include <chrono> #include <thread> #include <mutex> std::timed_mutex tm; void tryLockFunction() { if (tm.try_lock_for(std::chrono::seconds(1))) { std::cout << "Acquired lock" << std::endl; std::this_thread::sleep_for(std::chrono::seconds(2)); (); } else { std::cout << "Could not acquire lock in time" << std::endl; } } int main() { std::thread t1(tryLockFunction); std::thread t2(tryLockFunction); (); (); return 0; }
Recursive timed mutex (std::recursive_timed_mutex)
- This is a lock that combines the characteristics of recursive mutexes and timed mutexes. It allows the same thread to acquire the lock multiple times and can set the timeout when acquiring the lock.
- When a thread acquires such a lock multiple times, it needs to release the same number of locks before the lock will be truly released. In the process of acquiring the lock, if it cannot be acquired within the timeout time, the thread can take corresponding measures.
#include <iostream> #include <chrono> #include <thread> #include <mutex> std::recursive_timed_mutex rtm; void recursiveTryLockFunction(int count) { if (rtm.try_lock_for(std::chrono::seconds(1))) { std::cout << "Recursive acquired lock, count = " << count << std::endl; if (count > 0) { recursiveTryLockFunction(count - 1); } (); } else { std::cout << "Could not recursively acquire lock in time" << std::endl; } } int main() { std::thread t(recursiveTryLockFunction, 3); (); return 0; }
Spinlock (usually implemented with std::atomic_flag)
- Spinlock is a lock mechanism that is busy waiting. When a thread tries to acquire a spin lock and the lock has been occupied, the thread will not enter the blocking state, but will constantly check whether the ("spin") lock has been released.
- Spinlock may perform better when waiting time is short, because it avoids the overhead of thread switching. However, if the waiting time is too long, the threads are constantly occupying CPU resources for inspection, which will lead to waste of CPU resources. It is generally used in scenarios where the underlying code is required or has very high performance and the waiting time is expected to be very short.
#include <iostream> #include <atomic> #include <thread> std::atomic_flag spinLock = ATOMIC_FLAG_INIT; void criticalSection() { while (spinLock.test_and_set()) { // Spin waiting } std::cout << "Entered critical section" << std::endl; // Critical area operation (); } int main() { std::thread t1(criticalSection); std::thread t2(criticalSection); (); (); return 0; }
Condition variables (std::condition_variable) are used for synchronization with mutexes (strictly speaking, condition variables are not locks, but are often used together in thread synchronization scenarios)
- Condition variables are not a lock themselves, but they are usually used with mutexes to achieve synchronization between threads. It allows a thread to wait for a certain condition to be met before continuing to execute.
- For example, in a producer-consumer model, when the buffer is empty, the consumer thread can wait using a condition variable until the producer thread produces the product and notifies the consumer thread. In this process, the mutex is used to protect the shared resource of the buffer, and the condition variable is used to realize communication and synchronization between threads.
#include <iostream> #include <thread> #include <mutex> #include <condition_variable> #include <queue> std::mutex mtx; std::condition_variable cv; std::queue<int> buffer; const int bufferSize = 5; void producer() { for (int i = 0; i < 10; ++i) { std::unique_lock<std::mutex> lock(mtx); while (() == bufferSize) { (lock); } (i); std::cout << "Produced: " << i << std::endl; cv.notify_all(); } } void consumer() { for (int i = 0; i < 10; ++i) { std::unique_lock<std::mutex> lock(mtx); while (()) { (lock); } int data = (); (); std::cout << "Consumed: " << data << std::endl; cv.notify_all(); } } int main() { std::thread producerThread(producer); std::thread consumerThread(consumer); (); (); return 0; }
This is the end of this article about various locks in C++. For more related C++ content, please search for my previous articles or continue browsing the related articles below. I hope everyone will support me in the future!