DEV Community

Vivek Yadav
Vivek Yadav

Posted on

9

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.

Heroku

This site is built on Heroku

Join the ranks of developers at Salesforce, Airbase, DEV, and more who deploy their mission critical applications on Heroku. Sign up today and launch your first app!

Get Started

Top comments (1)

Sentry image

See why 4M developers consider Sentry, “not bad.”

Fixing code doesn’t have to be the worst part of your day. Learn how Sentry can help.

Learn more

👋 Kindness is contagious

Please leave a ❤️ or a friendly comment on this post if you found it helpful!

Okay