Mutex lock ensures synchronization between threads, but it turns parallel operation into serial operation, which has a great impact on performance, so we should minimize the locking area, that is, using fine-grained locks.
lock_guard does not work well and is not flexible enough. lock_guard only guarantees unlocking at the time of deconstruction. lock_guard itself does not provide an interface for locking and unlocking, but sometimes it does. Look at the following example.
class LogFile { std::mutex _mu; ofstream f; public: LogFile() { f.open("log.txt"); } ~LogFile() { f.close(); } void shared_print(string msg, int id) { { std::lock_guard<std::mutex> guard(_mu); //do something 1 } //do something 2 { std::lock_guard<std::mutex> guard(_mu); // do something 3 f << msg << id << endl; cout << msg << id << endl; } } };
In the above code, there are two pieces of code inside a function that need to be protected. At this time, using lock_guard, you need to create two local objects to manage the same mutex (in fact, you can only create one, but the lock is too powerful and inefficient). The modification method is to use unique_lock. It provides lock() and unlock() interfaces to record whether the lock is currently locked or unlocked. When destructing, it decides whether to unlock or not according to the current state (lock_guard will be unlocked). The above code is modified as follows:
class LogFile { std::mutex _mu; ofstream f; public: LogFile() { f.open("log.txt"); } ~LogFile() { f.close(); } void shared_print(string msg, int id) { std::unique_lock<std::mutex> guard(_mu); //do something 1 guard.unlock(); //Temporary unlock //do something 2 guard.lock(); //Continue to lock // do something 3 f << msg << id << endl; cout << msg << id << endl; // At the end, the destructor guard temporarily unlocks // If you don't write this sentence, it will be automatically executed when you destruct it. // guard.ulock(); } };
As can be seen from the above code, unlocking can be temporarily released when no locks are needed, and then continued when protection is needed, so that the lock_guard object can be instantiated without repetition, and the lock area can be reduced. Similarly, you can use std::defer_lock to set initialization without the default lock operation:
void shared_print(string msg, int id) { std::unique_lock<std::mutex> guard(_mu, std::defer_lock); //do something 1 guard.lock(); // do something protected guard.unlock(); //Temporary unlock //do something 2 guard.lock(); //Continue to lock // do something 3 f << msg << id << endl; cout << msg << id << endl; // At the end, the destructor guard temporarily unlocks }
This makes it more flexible to use than lock_guard! Then there is a cost, because it needs to maintain the lock state internally, so the efficiency is a little lower than lock_guard. When lock_guard can solve the problem, it uses lock_guard, and vice versa, uses unique_lock.
Later, when learning conditional variables, unique_lock will be useful.
In addition, please note that unique_lock and lock_guard can not be copied, lock_guard can not be moved, but unique_lock can!
// unique_lock can be moved, not replicated std::unique_lock<std::mutex> guard1(_mu); std::unique_lock<std::mutex> guard2 = guard1; // error std::unique_lock<std::mutex> guard2 = std::move(guard1); // ok // lock_guard cannot be moved and duplicated std::lock_guard<std::mutex> guard1(_mu); std::lock_guard<std::mutex> guard2 = guard1; // error std::lock_guard<std::mutex> guard2 = std::move(guard1); // error