C++
multithreading
mutex
unique_lock
lock_guard

stdunique_lockstdmutex or stdlock_guardstdmutex?

Interview Questions practice on Codemia

Over 8,000 real interview questions from top companies, searchable by company and role.

Browse interview questions

Introduction

In the world of concurrent programming in C++, managing access to shared resources is a critical challenge. Two utilities from the C++ Standard Library that aid in this endeavor are std::lock_guard<std::mutex> and std::unique_lock<std::mutex>. Both of these templates are designed to work with mutexes, ensuring that a piece of code operates with exclusive access to a resource. In this article, we will delve into the technical aspects of these two locking mechanisms, explore their differences, and examine situations where one might be preferred over the other.

The Basics of Mutex and Locking

Before we dive deeper, let's establish some foundational knowledge:

  • A mutex (short for mutual exclusion) is a locking mechanism used to synchronize access to a shared resource. Only one thread can hold the mutex at a time.
  • A lock is a higher-level construct that provides mechanisms to control a mutex's lock and unlock operations, ensuring thread safety.

Why Locking?

Concurrency can lead to problems such as race conditions, where the outcome of operations depends on the sequence or timing of thread execution. Proper mutex locking, facilitated by lock classes, mitigates such issues by ensuring only one thread accesses critical sections of code at any time.

std::lock_guard<std::mutex>

std::lock_guard<std::mutex> is a simple, RAII-style (Resource Acquisition Is Initialization) locking mechanism. It locks a mutex when created and automatically unlocks it when destroyed.

Characteristics of std::lock_guard

  • RAII Mechanism: The mutex is locked upon std::lock_guard creation and unlocked at its destruction.
  • Non-Assignable and Non-Copyable: To prevent multiple instances managing the same mutex lifetime.
  • Scope-Bound: The lock's duration is tied to the block in which it is declared.

Example Usage

cpp
1#include <iostream>
2#include <thread>
3#include <mutex>
4
5std::mutex mtx;
6
7void safe_increment(int& counter) {
8    std::lock_guard<std::mutex> lock(mtx);
9    ++counter;
10    std::cout << "Counter: " << counter << std::endl;
11}
12
13int main() {
14    int counter = 0;
15    std::thread t1(safe_increment, std::ref(counter));
16    std::thread t2(safe_increment, std::ref(counter));
17
18    t1.join();
19    t2.join();
20
21    return 0;
22}

In this example, std::lock_guard ensures that only one thread increments the counter and prints its value at a time.

std::unique_lock<std::mutex>

std::unique_lock<std::mutex> provides greater flexibility compared to std::lock_guard, offering more control over the mutex's lifecycle.

Characteristics of std::unique_lock

  • Flexibility: You can lock and unlock the mutex multiple times within the same lock's life.
  • Lock Management: Allows for deferred lock acquisition and condition variable integration.
  • Move-Assignable: Unlike std::lock_guard, it can be transferred between scopes through move semantics.

Example Usage

cpp
1#include <iostream>
2#include <thread>
3#include <mutex>
4#include <condition_variable>
5
6std::mutex mtx;
7std::condition_variable cv;
8bool ready = false;
9
10void worker(int id) {
11    std::unique_lock<std::mutex> lock(mtx);
12    cv.wait(lock, [] { return ready; });
13    std::cout << "Worker " << id << " is proceeding" << std::endl;
14}
15
16void notify() {
17    std::unique_lock<std::mutex> lock(mtx);
18    ready = true;
19    cv.notify_all();
20}
21
22int main() {
23    std::thread t1(worker, 1);
24    std::thread t2(worker, 2);
25
26    std::this_thread::sleep_for(std::chrono::seconds(1));
27    notify();
28
29    t1.join();
30    t2.join();
31
32    return 0;
33}

In this example, std::unique_lock works with the condition variable to manage complex thread interactions.

Key Differences Between lock_guard and unique_lock

Below is a table summarizing the key differences between std::lock_guard<std::mutex> and std::unique_lock<std::mutex>:

Featurestd::lock_guard<std::mutex>std::unique_lock<std::mutex>
Locking MechanismLocks in constructor, unlocks in destructorCan defer locking, manual lock/unlock
RAII StyleYesYes
Lock/Unlock ControlNoYes, via methods
Move OperationsNot allowedAllowed (movable)
Condition Variable SupportLimitedFull support
Code ComplexitySimpleMore complex due to additional features

When to Use Which?

  • Use std::lock_guard when simple lock management is needed, where the scope of the lock is clear and requires no additional operations.
  • Use std::unique_lock in scenarios demanding flexibility, like working with condition variables or multiple lock/unlock operations within the same scope.

Conclusion

Selecting between std::lock_guard and std::unique_lock involves considering the specific concurrency requirements of your application. While std::lock_guard offers simplicity and ease of use, std::unique_lock provides the additional control needed for more complex scenarios. Understanding the strengths and limitations of each will help you write efficient, safe, and maintainable concurrent C++ applications.


Related reading
Free course
Beginner
7 lessons
2 hours
Tackling System Design Interview Problems

A short course that equips you with the skills to approach system design interviews methodically.

Start the free course
Track what you have practised

A free account saves your progress, solutions and study plan across every problem on Codemia.

Interview Questions practice on Codemia

Over 8,000 real interview questions from top companies, searchable by company and role.

Browse interview questions

All Rights Reserved.