2023-05-23 11:04:00
Les modèles sont une abstraction importante dans le développement de logiciels modernes. Ils offrent une terminologie bien définie, une documentation propre et l’apprentissage des meilleurs. Cet article s’attarde davantage sur les modèles de concurrence. Le verrouillage est un moyen classique de protéger un état partagé et modifiable. Aujourd’hui je vous présente les deux variantes : Scoped Locking et Strategized Locking.
Rainer Grimm travaille depuis de nombreuses années en tant qu’architecte logiciel, chef d’équipe et responsable de la formation. Il aime écrire des articles sur les langages de programmation C++, Python et Haskell, mais aime aussi intervenir fréquemment lors de conférences spécialisées. Sur son blog Modernes C++, il traite intensément de sa passion pour le C++.
Une idée simple existe avec le verrouillage pour protéger une section critique. Une section critique est un morceau de code qu’un thread doit utiliser exclusivement.
Verrouillage de portée
Le verrouillage de portée est le concept de RAIIque sur un mutex est appliqué. Le verrouillage à portée est également connu sous le nom de blocage et de protection synchronisés. L’essence de cet idiome est de lier l’acquisition et la libération de ressources à la durée de vie d’un objet. Comme son nom l’indique, la durée de vie de l’objet est délimitée. Scoped signifie que le runtime C++ est responsable de la destruction de l’objet et donc de la libération de la ressource.
La classe ScopedLock
implémenter le Scoped Locking.
// scopedLock.cpp
#include
#include
#include
#include
class ScopedLock{
private:
std::mutex& mut;
public:
explicit ScopedLock(std::mutex& m): mut(m){ // (1)
mut.lock(); // (2)
std::cout << "Lock the mutex: " << &mut << 'n';
}
~ScopedLock(){
std::cout << "Release the mutex: " << &mut << 'n';
mut.unlock(); // (3)
}
};
int main(){
std::cout << 'n';
std::mutex mutex1;
ScopedLock scopedLock1{mutex1};
std::cout << "nBefore local scope" << 'n';
{
std::mutex mutex2;
ScopedLock scopedLock2{mutex2};
} // (4)
std::cout << "After local scope" << 'n';
std::cout << "nBefore try-catch block" << 'n';
try{
std::mutex mutex3;
ScopedLock scopedLock3{mutex3};
throw std::bad_alloc();
} // (5)
catch (std::bad_alloc& e){
std::cout << e.what();
}
std::cout << "nAfter try-catch block" << 'n';
std::cout << 'n';
}
ScopedLock
obtient son mutex par référence (1). Le mutex est verrouillé dans le constructeur (2) et déverrouillé dans le destructeur (3). Grâce à l'idiome RAII, l'objet est détruit et le mutex est libéré automatiquement.
La portée de scopedLock1
se termine à la fin de main
-Fonction. sera par conséquent mutex1
déverrouillé. La même chose s'applique mutex2
et mutex3
. Ils sont automatiquement libérés à la fin de leur périmètre local (4 et 5). À mutex3
devient aussi le destructeur de scopedLock3
appelée lorsqu'une exception se produit. Intéressant c'est que mutex3
le souvenir de mutex2
réutilisés car les deux ont la même adresse.
Le verrouillage à portée présente les avantages et les inconvénients suivants :
Avantages:
- Robustesse car le verrou est acquis et relâché automatiquement.
Désavantages:
- Le curling récursif d'un
std::mutex
est un comportement indéfini et conduit généralement à un blocage.
- Les verrous ne sont pas automatiquement libérés lorsque la fonction C longjmp est utilisé; longjpm n'appelle pas les destructeurs C++ d'objets délimités.
C++17 prend en charge les verrous en quatre versions. C++ en a un std::lock_guard
/ std::scoped_lock
pour les simples et un std::unique_lock
/ std::shared_lock
pour les cas d'utilisation avancés comme le verrouillage ou le déverrouillage explicite du mutex. Plus d'informations sur le mutex et les verrous dans mon article "Serrures".
Strategized Locking setzt gerne Scoped Locking an.
Verrouillage stratégique
Supposons qu'un code tel qu'une bibliothèque doive également être utilisé simultanément dans différents domaines. Pour plus de sécurité, protégez les sections critiques avec un cadenas. Si la bibliothèque s'exécute maintenant dans un environnement à thread unique, un problème de performances se pose car un mécanisme de synchronisation coûteux est utilisé qui n'est pas nécessaire. C'est là qu'intervient le verrouillage stratégique : appliquer le modèle de stratégie au verrouillage. Cela signifie que vous enveloppez votre stratégie de verrouillage dans un objet et en faites un composant de votre système.
Deux méthodes typiques pour implémenter le verrouillage stratégique sont le polymorphisme d'exécution (orientation objet) ou le polymorphisme de compilation (modèles). Les deux méthodes améliorent la personnalisation et l'extension de la stratégie de verrouillage, facilitent la maintenance du système et prennent en charge la réutilisation des composants. L'implémentation du verrouillage stratégique au moment de l'exécution ou de la compilation diffère sur plusieurs aspects.
Avantages :
Polymorphisme à l'exécution
- permet de configurer la stratégie de verrouillage lors de l'exécution, et
- est plus facile à comprendre pour les développeurs venant d'un milieu orienté objet.
Polymorphisme à la compilation
- n'a pas de désavantage d'abstraction et
- a une hiérarchie plate.
Désavantages:
Polymorphisme à l'exécution
- nécessite une indirection de pointeur supplémentaire et
- peut avoir une hiérarchie de dérivation profonde.
Polymorphisme à la compilation
- peut générer de longs messages d'erreur en cas d'erreur (cela change avec des concepts tels que
BasicLockable
en C++20).
Après cette discussion théorique, je vais implémenter le verrouillage stratégique dans les deux variantes. Dans mon exemple, le verrouillage stratégique prend en charge le verrouillage aucun, exclusif et partagé. Pour plus de simplicité, j'ai utilisé des mutex préexistants.
Polymorphisme à l'exécution
Le programme strategizedLockingRuntime.cpp
utilise trois stratégies de verrouillage différentes.
// strategizedLockingRuntime.cpp
#include
#include
#include
class Lock { // (4)
public:
virtual void lock() const = 0;
virtual void unlock() const = 0;
};
class StrategizedLocking {
Lock& lock; // (1)
public:
StrategizedLocking(Lock& l): lock(l){ // (2)
lock.lock();
}
~StrategizedLocking(){ // (3)
lock.unlock();
}
};
struct NullObjectMutex{
void lock(){}
void unlock(){}
};
class NoLock : public Lock { // (5)
void lock() const override {
std::cout << "NoLock::lock: " << 'n';
nullObjectMutex.lock();
}
void unlock() const override {
std::cout << "NoLock::unlock: " << 'n';
nullObjectMutex.unlock();
}
mutable NullObjectMutex nullObjectMutex; // (10)
};
class ExclusiveLock : public Lock { // (6)
void lock() const override {
std::cout << " ExclusiveLock::lock: " << 'n';
mutex.lock();
}
void unlock() const override {
std::cout << " ExclusiveLock::unlock: " << 'n';
mutex.unlock();
}
mutable std::mutex mutex; // (11)
};
class SharedLock : public Lock { // (7)
void lock() const override {
std::cout << " SharedLock::lock_shared: " << 'n';
sharedMutex.lock_shared(); // (8)
}
void unlock() const override {
std::cout << " SharedLock::unlock_shared: " << 'n';
sharedMutex.unlock_shared(); // (9)
}
mutable std::shared_mutex sharedMutex; // (12)
};
int main() {
std::cout << 'n';
NoLock noLock;
StrategizedLocking stratLock1{noLock};
{
ExclusiveLock exLock;
StrategizedLocking stratLock2{exLock};
{
SharedLock sharLock;
StrategizedLocking startLock3{sharLock};
}
}
std::cout << 'n';
}
La classe StrategizedLocking
comporte une serrure (1). StrategizedLocking
modélise le verrouillage de portée et verrouille donc le mutex dans le constructeur (2) et le libère à nouveau dans le destructeur (3). Lock
(4) est une classe abstraite et définit l'interface des classes dérivées. Ce sont les classes NoLock (5), ExclusiveLock
(6) et SharedLock
(7). SharedLock
appels lock_shared
(8) et unlock_shared
(9) sur son std::shared_mutex
sur. Chacun de ces verrous contient l'un des mutex NullObjectMutex
(dix), std::mutex
(11) ou std::shared_mutex
(ligne 12). NullObjectMutex
est un espace réservé noop. Les mutex sont stockés comme mutable
déclaré. Par conséquent, ils sont dans des fonctions membres constantes comme lock
et unlock
utilisable.
Polymorphisme à la compilation
L'implémentation basée sur un modèle est très similaire à l'implémentation orientée objet. Au lieu d'une classe de base abstraite Lock
Je définis le concept BasicLockable.
Vous trouverez plus d'informations sur les concepts dans mes articles précédents : Notions.
template
concept BasicLockable = requires(T lo) {
lo.lock();
lo.unlock();
};
BasicLockable
requis par son paramètre de type T,
qu'il a les fonctions de membre lock
et unlock
mis en œuvre. Par conséquent, le modèle de classe accepte StrategizedLocking
ne tapez que les paramètres qui satisfont cette contrainte.
template
class StrategizedLocking {
...
Enfin, l'implémentation basée sur un modèle suit.
// strategizedLockingCompileTime.cpp
#include
#include
#include
template
concept BasicLockable = requires(T lo) {
lo.lock();
lo.unlock();
};
template
class StrategizedLocking {
Lock& lock;
public:
StrategizedLocking(Lock& l): lock(l){
lock.lock();
}
~StrategizedLocking(){
lock.unlock();
}
};
struct NullObjectMutex {
void lock(){}
void unlock(){}
};
class NoLock{
public:
void lock() const {
std::cout << "NoLock::lock: " << 'n';
nullObjectMutex.lock();
}
void unlock() const {
std::cout << "NoLock::unlock: " << 'n';
nullObjectMutex.lock();
}
mutable NullObjectMutex nullObjectMutex;
};
class ExclusiveLock {
public:
void lock() const {
std::cout << " ExclusiveLock::lock: " << 'n';
mutex.lock();
}
void unlock() const {
std::cout << " ExclusiveLock::unlock: " << 'n';
mutex.unlock();
}
mutable std::mutex mutex;
};
class SharedLock {
public:
void lock() const {
std::cout << " SharedLock::lock_shared: " << 'n';
sharedMutex.lock_shared();
}
void unlock() const {
std::cout << " SharedLock::unlock_shared: " << 'n';
sharedMutex.unlock_shared();
}
mutable std::shared_mutex sharedMutex;
};
int main() {
std::cout << 'n';
NoLock noLock;
StrategizedLocking stratLock1{noLock};
{
ExclusiveLock exLock;
StrategizedLocking stratLock2{exLock};
{
SharedLock sharLock;
StrategizedLocking startLock3{sharLock};
}
}
std::cout << 'n';
}
Les programmes strategizedLockingRuntime.cpp
et strategizedLockingCompileTime.cpp
produire la même sortie :
Et après?
Guarded Suspension utilise une stratégie différente pour faire face au changement. Il signale quand le changement a eu lieu. Dans mon prochain article, j'entrerai plus en détail sur la suspension surveillée.
(rme)
#Développement #logiciel #faire #face #changement #Verrouillage
1684861095