DEV Community

Vivek Yadav
Vivek Yadav

Posted on

Understanding std::unique_lock and std::shared_lock in C++

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 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;
}
Enter fullscreen mode Exit fullscreen mode

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;
}
Enter fullscreen mode Exit fullscreen mode

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;
}
Enter fullscreen mode Exit fullscreen mode

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.

Top comments (0)