Je suis tombé sur un std::shared_mutex
dans C++17
. Qu'est-ce exactement que std::shared_mutex
et en quoi est-il différent de std::mutex
?
Comme indiqué dans la documentation
La classe shared_mutex est une primitive de synchronisation qui peut être utilisée pour protéger les données partagées d'un accès simultané par plusieurs threads. Contrairement aux autres types de mutex qui facilitent l'accès exclusif, un shared_mutex a deux niveaux d'accès:
- shared - plusieurs threads peuvent partager la propriété du même mutex .
- exclusif - un seul thread peut posséder le mutex.
Les mutex partagés sont généralement utilisés dans des situations où plusieurs lecteurs peuvent accéder à la même ressource en même temps sans provoquer de course des données, mais un seul rédacteur peut le faire.
Cela a plusieurs utilisations, mais l’une des plus communes est d’implémenter un Read Write Lock dans lequel plusieurs threads peuvent lire des données partagées, mais un seul thread écrit à la fois exclusivement. Ainsi, lorsque vous avez plusieurs lecteurs, le mutex agit en "mode partagé", mais lorsqu'une écriture est demandée, il passe en "mode exclusif".
std::shared_mutex
peut être utile en particulier dans les cas où la structure de données (comme le cache DNS) obtient rarement mis à jour . Utiliser un std::mutex
pour protéger la structure de données peut être excessivement pessimiste, car cela élimine la possibilité de simultanéité lors de la lecture de la structure de données Lorsque celle-ci n’est pas en cours de modification. Plusieurs threads peuvent avoir un verrou partagé sur le même std::shared_mutex
en même temps.
Un exemple tiré du livre d'Anthony Williams:
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;
}
};
Ici, la fonction find_entry
effectue essentiellement l'opération de lecture, alors que update_or_add_entry
effectue l'opération d'écriture.
Donc, on peut dire que std::shared_mutex
est un lecteur-écrivain mutex typique, car il autorise Deux types d’utilisation différents: accès exclusif par un seul thread «écrivain» ou partagé, accès simultané par plusieurs threads «lecteur».
Une mutex
est verrouillée ou non.
Un shared_mutex
est soit verrouillé exclusivement, soit verrouillé partagé, ou non.
N'importe quel nombre de clients peut partager verrouiller un mutex partagé.
Si quelqu'un a l'exclusivité verrouillée, personne d'autre ne peut détenir any locks.
Sur les fenêtres, il s'agit du type SWRLOCK
- et en fait, ce verrou est généralement utilisé pour implémenter des verrous en lecture-écriture; beaucoup de lecteurs sont autorisés, mais l'écriture doit être exclusive.
Voici un exemple de code permettant de créer deux wrappers de modèles pour des mutex partagés et non partagés. Dans un cas, nous avons lu et écrit des opérations qui acquièrent des verrous différents. Dans l'autre, nous avons juste accès:
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); }
};