Locking with std::unique_lock
Compared to std::lock_guard, std::unique_lock provides a bit more flexibility in operations. An std::unique_lock instance doesn't always own a mutex associated with it. Firstly, you can pass std::adopt_lock as a second argument to the constructor to manage a lock on a mutex similar to std::lock_guard. Secondly, the mutex can remain unlocked during construction by passing std::defer_lock as a second argument to the constructor. So, later in the code, a lock can be acquired by calling lock() on the same std::unique_lock object. But the flexibility available with std::unique_lock comes with a price; it is a bit slower than lock_guard in regards to storing this extra information and is in need of an update. Therefore, it is recommended to use lock_guard unless there is a real need for the flexibility that std::unique_lock offers.
Another interesting feature about std::unique_lock is its ability to transfer ownership. Since std::unique_lock must own its associated mutexes, this results in the ownership transfer of mutexes. Similar to std::thread, the std::unique_lock class is also a move only type. All of the move semantic language nuances and rvalue reference handling available in the C++ standard library applies to std::unique_lock as well.
The availability of member functions such as lock() and unlock(), similar to std::mutex, increases the flexibility of its use in code compared to std::lock_guard. The ability to release the lock before an std::unique_lock instance is destroyed, meaning that you can optionally release it anywhere in the code if it's obvious that the lock is no longer required. Holding down the lock unnecessarily can drop the performance of the application drastically, since the threads waiting for locks are prevented from executing for longer than is necessary. Hence, std::unique_lock is a very handy feature introduced by the C++ standard library, which supports RAII idiom, and it can effectively minimize the size of a critical section of the applicable code:
void retrieve_and_process_data(data_params param) { std::unique_lock<std::mutex> local_lock(global_mutex, std::defer_lock); prepare_data(param); local_lock.lock(); data_class data = get_data_to_process(); local_lock.unlock(); result_class result = process_data(data); local_lock.lock(); strore_result(result); }
In the preceding code, you can see the fine-grained locking achieved by leveraging the flexibility of std::unique_lock. As the function starts its execution, an std::unique_lock object is constructed with global_mutex in an unlocked state. Immediately, data is prepared with params, which don't require exclusive access; it is executing freely. Before retrieving the prepared data, the local_lock is marking the beginning of a critical section using the lock member function in std::unique_lock. As soon as the data retrieval is over, the lock is released, marking the end of the critical section. Followed by that, a call to the process_data() function, which again does not require exclusive access, is getting executed freely. Finally, before the execution of the store_result() function, the mutex is locked to protect the write operation, which updates the processed result. When exiting the function, the lock gets released when the local instance of std::unique_lock is destroyed.