Reader-Writer Locks in C++ with std::shared_mutex
Introduced in C++17, std::shared_mutex provides a native
mechanism to implement the classic reader-writer synchronization
pattern. It solves concurrency bottlenecks in read-heavy applications by
allowing multiple threads to read shared data concurrently while
restricting write operations to a single thread. This article explains
how std::shared_mutex separates read and write operations,
the RAII wrappers used to manage it, and when this locking model is most
effective.
The Reader-Writer Problem
A standard std::mutex provides mutual exclusion: only
one thread can access a critical section at any given time, regardless
of whether it is reading or modifying data. In systems where reads
vastly outnumber writes, this behavior creates unnecessary contention
because concurrent reads do not modify state and are inherently safe to
run simultaneously.
The reader-writer pattern addresses this inefficiency by defining two distinct access levels:
- Shared (Read) Access: Multiple threads can acquire the lock concurrently, provided no thread is modifying the resource.
- Exclusive (Write) Access: Only one thread can acquire the lock, blocking all other readers and writers until the operation finishes.
How std::shared_mutex Implements Dual Locking
The std::shared_mutex primitive, located in the
<shared_mutex> header, provides distinct member
functions to handle both access types:
- Exclusive Locking: Methods such as
lock(),try_lock(), andunlock()function identically to standard mutex operations. They are reserved for write operations that alter shared state. - Shared Locking: Methods such as
lock_shared(),try_lock_shared(), andunlock_shared()allow multiple threads to acquire shared ownership. As long as at least one thread holds shared ownership, no thread can acquire exclusive ownership.
If a thread attempts to acquire an exclusive lock while shared locks are active, it blocks until all readers release their locks. Conversely, if a thread holds an exclusive lock, any new reader or writer is blocked until the exclusive lock is released.
Using RAII Lock Wrappers
Directly calling lock() or lock_shared() is
error-prone because exceptions or early returns can cause deadlocks.
Modern C++ relies on RAII (Resource Acquisition Is Initialization)
wrappers to manage lock lifetimes automatically.
1. std::shared_lock
for Readers
std::shared_lock is designed specifically for shared
ownership. When instantiated, it calls lock_shared() on the
underlying mutex and automatically calls unlock_shared()
when it goes out of scope.
2.
std::unique_lock or std::lock_guard for
Writers
Writers require exclusive ownership. std::unique_lock or
std::lock_guard calls lock() and
unlock(), guaranteeing that no other reader or writer
executes concurrently.
#include <iostream>
#include <shared_mutex>
#include <string>
#include <unordered_map>
class ThreadSafeCache {
public:
std::string get(const std::string& key) const {
// Shared lock: multiple readers can execute this simultaneously
std::shared_lock<std::shared_mutex> lock(mutex_);
auto it = data_.find(key);
return (it != data_.end()) ? it->second : "";
}
void set(const std::string& key, const std::string& value) {
// Exclusive lock: blocks all other readers and writers
std::unique_lock<std::shared_mutex> lock(mutex_);
data_[key] = value;
}
private:
mutable std::shared_mutex mutex_;
std::unordered_map<std::string, std::string> data_;
};Performance Trade-Offs
While std::shared_mutex improves read throughput in
specific scenarios, it introduces higher runtime overhead than a basic
std::mutex due to internal reference counting and state
tracking.
std::shared_mutex is optimal when:
- The read-to-write ratio is high (e.g., 90% or more reads).
- The read operations take significant time to execute, amortizing the synchronization overhead.
If read operations are extremely short (such as reading a single
integer) or if write frequency is high, a standard
std::mutex or an atomic type (std::atomic)
will typically outperform std::shared_mutex. Additionally,
lock scheduling policies—such as whether readers or writers are
prioritized under heavy contention—depend on the underlying platform's
operating system implementation.