Objet verrouillable – un type Guarded<T>
Supposons que nous souhaitons non pas utiliser une classe combinant un outil
de synchronisation et un objet à synchroniser, comme dans le cas d'une
zone de transit, masi bien un
objet lui-même verrouillable, offrant une interface classique comme celle d'un
mutex (le trio lock(),
try_lock(), unlock()) de manière à ce qu'il
soit possible de le manipuler à la fois comme la donnée qu'il encapsule et comme
le verrou qui le protège.
Le truc est tout simple :
- Encapsuler le mutex et l'objet dans
une même classe, disons Guarded<T> pour un type
T donné
- Offrir l'interface du mutex par délégation, et
- Offrir l'interface du T par l'opérateur
->. Notez ici qu'il aurait pu être sage d'utiliser un
observer_ptr plutôt qu'un pointeur brut, bien que ce ne soit pas
dramatique
|
#include <mutex>
template <class T>
class Guarded {
mutable std::mutex m;
T obj;
public:
template <class U>
Guarded(U && obj) : obj { std::forward<U>(obj) } {
}
auto try_lock() { return m.lock(); }
void lock() { m.lock(); }
void unlock() { m.unlock(); }
auto operator->() { return &obj; }
auto operator->() const { return &obj; }
};
template <class T> Guarded(T) -> Guarded<T>;
|
Le code en exemple à droite montre comment il serait possible de
verrouiller une Guarded<std::string> puis d'en
appeler la méthode size() de manière
synchronisée. |
#include <string>
#include <iostream>
int main() {
using namespace std;
auto s = Guarded("Yo"s);
lock_guard _{ s };
cout << s->size() << endl;
}
|
Et si nous souhaitions nous assurer que les accès à la ressource ne soient
faits que par un thread détenteur du verrou, pour
dépister des bogues? C'est à peine plus difficile : l'idée est de savoir,
avant d'accéder à l'objet verrouillable, si le thread
courant est propriétaire du verrou.
La technique pour y arriver est simple :
- Chaque thread a un identifiant unique, de
type thread::id
- Notre Guarded<T> garde un tel identifiant à
l'interne
- Chaque verrouillage réussi est suivi de la capture de l'identifiant du
thread actif
- Chaque déverrouillage est précédé par la réinitialisation de cet
identifiant
- Chaque tentative d'accès à l'objet encapsulé valide d'abord que le
thread actif est propriétaire du verrou, et lève une exception si
tel n'est pas le cas
C'est aussi simple que ça
|
#include <mutex>
#include <thread>
class not_owner{};
template <class T>
class Guarded {
mutable std::mutex m;
std::thread::id id{};
T obj;
void ensure_ownership() {
if (id != std::this_thread::get_id()) throw not_owner{};
}
public:
template <class U>
Guarded(U && obj) : obj { std::forward<U>(obj) } {
}
auto try_lock() {
if(auto ok = m.lock(); ok) {
id = std::this_thread::get_id();
}
return m.lock();
}
void lock() {
m.lock();
id = std::this_thread::get_id();
}
void unlock() {
id = {};
m.unlock();
}
auto operator->() {
ensure_ownership();
return &obj;
}
auto operator->() const {
ensure_ownership();
return &obj;
}
};
template <class T> Guarded(T) -> Guarded<T>;
|