I came across an std::shared_mutex
in C++17
. what exactly is std::shared_mutex
and how it is different from std::mutex
?
As noted in the documentation
The shared_mutex class is a synchronization primitive that can be used to protect shared data from being simultaneously accessed by multiple threads. In contrast to other mutex types which facilitate exclusive access, a shared_mutex has two levels of access:
- shared - several threads can share ownership of the same mutex.
- exclusive - only one thread can own the mutex.
Shared mutexes are usually used in situations when multiple readers can access the same resource at the same time without causing data races, but only one writer can do so.
This has a variety of uses, but one common one is to implement a Read Write Lock where you could have multiple threads reading shared data, but only one thread exclusively writing at any time. So when you have multiple readers the mutex acts in "shared mode", but when a write is requested it changes into "exclusive mode".
std::shared_mutex
can be useful especially in cases where data structure (like DNS cache) gets rarely updated. Using a std::mutex
to protect the data structure could be overly pessimistic, because it eliminates the possible concurrency in reading the data structure
when it isn’t undergoing modification. Multiple threads can have a shared lock on the same std::shared_mutex
at the same time.
One such example from Anthony Williams book:
class dns_cache
{
std::map<std::string,dns_entry> entries;
mutable boost::shared_mutex entry_mutex;
public:
dns_entry find_entry(std::string const& domain) const
{
boost::shared_lock<boost::shared_mutex> lk(entry_mutex);
std::map<std::string,dns_entry>::const_iterator const it = entries.find(domain);
return (it==entries.end()) ? dns_entry() : it->second;
}
void update_or_add_entry(std::string const& domain,
dns_entry const& dns_details)
{
std::lock_guard<boost::shared_mutex> lk(entry_mutex);
entries[domain] = dns_details;
}
};
Here, function find_entry
basically does the Read operation, whereas update_or_add_entry
performs the Write operation.
So, it can said that std::shared_mutex
is a typical reader-writer mutex, because it allows for
two different kinds of usage: exclusive access by a single “writer” thread or shared,
concurrent access by multiple “reader” threads.
A mutex
is either locked or not.
A shared_mutex
is either locked exclusively, or locked shared, or not.
Any number of clients can shared lock a shared mutex.
If anyone has it exclusive locked, nobody else can hold any locks.
On windows, this is the SWRLOCK
type -- and in fact, this lock is typically used to implement read-write locks; many readers allowed, but writing must be exclusive.
Here is some sample code to create two template wrappers for shared and non-shared mutexes. In one case, we have read and write operations that aquire different locks. In the other, we just have access:
template<class T, class M=std::mutex>
struct mutex_guarded {
template<class F>
auto access( F&& f ) {
auto l = lock();
return std::forward<F>(f)(t);
}
template<class F>
auto access( F&& f ) const {
auto l = lock();
return std::forward<F>(f)(t);
}
mutex_guarded(mutex_guarded const&)=delete;
mutex_guarded& operator=(mutex_guarded const&)=delete;
template<class...Ts>
mutex_guarded( Ts&&...ts ):t(std::forward<Ts>(ts)...){}
mutex_guarded()=default;
protected:
mutable M m;
T t;
auto lock() { return std::unique_lock<M>(m); }
};
template<class T, class M=std::shared_mutex>
struct shared_mutex_guarded:private mutex_guarded<T, M> {
using base = mutex_guarded<T, M>;
template<class F>
auto read( F&& f ) const { return access(std::forward<F>(f)); }
template<class F>
auto write( F&& f ) { return access(std::forward<F>(f)); }
using base::base;
protected:
using base::access;
template<class F>
auto access( F&& f ) const {
auto l = lock();
return std::forward<F>(f)(this->t);
}
using base::lock;
auto lock() const { return std::shared_lock<M>(this->m); }
};