Understanding std::unique_lock and std::shared_lock in C++
Tags: multithreading, cpp, criticalsection, locks
Published: 2024-07-09 06:27:10

Concurrency in programming allows multiple threads to execute code simultaneously, which can significantly improve the performance of applications, especially on multi-core processors. However, with the increased complexity of managing multiple threads, ensuring thread safety becomes crucial. C++ provides several synchronization primitives to manage access to shared resources in a multithreaded environment, including std::unique_lock and std::shared_lock. 

## 1. Introduction to Mutexes and Locks

A mutex (short for mutual exclusion) is a synchronization primitive that allows only one thread to access a resource at a time, ensuring that concurrent operations do not interfere with each other. In C++, mutexes are provided by the <mutex> header, and they can be used with various types of locks to control access.

A lock is an object that manages the ownership of a mutex. By using locks, programmers can ensure that only one thread accesses the protected resource at a time, preventing race conditions and ensuring data integrity.

## 2. std::unique_lock

std::unique_lock is a type of lock that provides exclusive ownership of a mutex. This means that only one thread can hold a std::unique_lock on a particular mutex at any given time. When a thread acquires a std::unique_lock, it has exclusive access to the resource protected by the mutex, blocking other threads from acquiring the same mutex until the lock is released.

**Key Characteristics:**


**Exclusive Ownership**: Only one thread can hold the lock at a time.
**Flexible Lock Management**: std::unique_lock provides various constructors and member functions to manage the lock, such as deferred locking, timed locking, and lock ownership transfer.
**RAII**: The lock follows the RAII (Resource Acquisition Is Initialization) idiom, automatically releasing the mutex when the std::unique_lock object is destroyed.

**Example**:

```
#include <iostream>
#include <thread>
#include <mutex>

std::mutex mtx;

void thread_function() {
    std::unique_lock<std::mutex> lock(mtx);
    // Critical section
    std::cout << "Thread " << std::this_thread::get_id() << " has the lock.\n";
    // The lock is automatically released when 'lock' goes out of scope
}

int main() {
    std::thread t1(thread_function);
    std::thread t2(thread_function);

    t1.join();
    t2.join();

    return 0;
}
```
In this example, std::unique_lock ensures that only one thread can execute the critical section at a time.

## 3. std::shared_lock

std::shared_lock is a type of lock that provides shared ownership of a std::shared_mutex. Unlike std::unique_lock, multiple threads can hold a std::shared_lock on the same std::shared_mutex simultaneously. This is useful in scenarios where multiple threads need to read a shared resource concurrently without modifying it.

**Key Characteristics:**


**Shared Ownership**: Multiple threads can hold the lock at the same time.
**Read-Only Access**: Suitable for scenarios where threads only need to read a shared resource.
**Compatibility with std::shared_mutex**: Works with std::shared_mutex, which supports both shared and exclusive locking.

**Example**:

```
#include <iostream>
#include <thread>
#include <shared_mutex>

std::shared_mutex sh_mtx;

void read_function() {
    std::shared_lock<std::shared_mutex> lock(sh_mtx);
    // Shared (read) access to the resource
    std::cout << "Thread " << std::this_thread::get_id() << " is reading.\n";
    // The lock is automatically released when 'lock' goes out of scope
}

int main() {
    std::thread t1(read_function);
    std::thread t2(read_function);

    t1.join();
    t2.join();

    return 0;
}
```

In this example, std::shared_lock allows both threads to hold the lock simultaneously, enabling concurrent read access to the shared resource.

## 4. Key Differences Between std::unique_lock and std::shared_lock

**1. Mutex Type:**

- std::unique_lock works with std::mutex, std::timed_mutex, std::recursive_mutex, and std::recursive_timed_mutex.

- std::shared_lock works specifically with std::shared_mutex (or std::shared_timed_mutex).

**2. Locking Behavior:**

- std::unique_lock provides exclusive access. Only one std::unique_lock can hold the mutex at a time.

- std::shared_lock provides shared access. Multiple std::shared_lock instances can hold the mutex simultaneously, but it cannot coexist with a std::unique_lock on the same mutex.

**3. Use Case:**

- Use std::unique_lock when you need to write or modify a shared resource.

- Use std::shared_lock when you only need to read a shared resource and want to allow other readers to access it concurrently.

**4. Performance:**

- std::shared_lock can improve performance in scenarios with many readers and few writers, as it allows concurrent reads, reducing contention compared to std::unique_lock.

## 5. Appropriate Use Cases

**1. std::unique_lock:**
- When modifying a shared resource.
- When performing operations that must not be interrupted by other threads.
- When exclusive access to a resource is required.

**2. std::shared_lock:**

- When reading a shared resource.
- When multiple threads need to read data simultaneously.
- When reducing contention in read-heavy workloads.

## 6. Combining std::shared_lock and std::unique_lock

In real-world applications, it is common to have a mix of read and write operations on shared resources. std::shared_mutex allows combining std::shared_lock for read operations and std::unique_lock for write operations, providing a balance between concurrency and safety.

**Example**:

```
#include <iostream>
#include <thread>
#include <shared_mutex>
#include <vector>

std::shared_mutex sh_mtx;
std::vector<int> shared_data;

void write_function() {
    std::unique_lock<std::shared_mutex> lock(sh_mtx);
    shared_data.push_back(1);  // Modify the shared resource
    std::cout << "Thread " << std::this_thread::get_id() << " has written data.\n";
}

void read_function() {
    std::shared_lock<std::shared_mutex> lock(sh_mtx);
    for (int value : shared_data) {
        std::cout << "Thread " << std::this_thread::get_id() << " read value: " << value << "\n";
    }
}

int main() {
    std::thread writer(write_function);
    std::thread reader1(read_function);
    std::thread reader2(read_function);

    writer.join();
    reader1.join();
    reader2.join();

    return 0;
}
```

In this example, the writer thread uses std::unique_lock to modify the shared data, while the reader threads use std::shared_lock to read the data concurrently.

## 7. Conclusion

Understanding the differences between std::unique_lock and std::shared_lock is essential for writing efficient and thread-safe C++ programs. std::unique_lock provides exclusive access to a resource, suitable for write operations, while std::shared_lock allows multiple threads to read a resource concurrently, enhancing performance in read-heavy scenarios. By using these locks appropriately, developers can ensure data integrity and optimize the performance of their multithreaded applications.