🔝 Retour au Sommaire
std::unique_lock est le couteau suisse du verrouillage en C++. Là où std::lock_guard impose un verrouillage strict du constructeur au destructeur, std::unique_lock offre un contrôle total : verrouillage différé, déverrouillage anticipé, tentatives avec timeout, déplacement entre scopes, et compatibilité avec std::condition_variable.
Cette flexibilité a un prix minime — std::unique_lock maintient un booléen interne pour savoir si le mutex est actuellement détenu — mais elle ouvre des possibilités que lock_guard ne peut pas offrir.
#include <mutex>
std::mutex mtx;
void flexible_operation() {
std::unique_lock lock(mtx); // Verrouille immédiatement (comme lock_guard)
// ... section critique ...
lock.unlock(); // Déverrouillage anticipé — impossible avec lock_guard
// ... travail local non protégé ...
lock.lock(); // Re-verrouillage
// ... nouvelle section critique ...
} // Destructeur : unlock() si le mutex est encore détenuLa puissance de std::unique_lock vient en grande partie de ses multiples constructeurs. Chacun correspond à une stratégie de verrouillage différente.
Le comportement par défaut est identique à lock_guard — le mutex est verrouillé dans le constructeur :
std::unique_lock lock(mtx); // mtx.lock() appelé immédiatementLe mutex est associé au unique_lock mais pas verrouillé. Vous le verrouillerez plus tard, manuellement ou via std::lock() :
std::unique_lock lock(mtx, std::defer_lock);
// mtx n'est PAS verrouillé ici
// lock.owns_lock() == false
// ... préparation locale ...
lock.lock(); // Verrouillage explicite quand on est prêt
// lock.owns_lock() == trueCe mode est particulièrement utile pour le verrouillage simultané de plusieurs mutex (voir plus bas).
Comme pour lock_guard, ce mode indique que le mutex est déjà verrouillé par le thread courant. Le unique_lock prend en charge le unlock() à sa destruction, sans appeler lock() dans le constructeur :
mtx.lock(); // Verrouillage manuel préalable
std::unique_lock lock(mtx, std::adopt_lock);
// lock gère désormais le unlock()Le constructeur tente d'acquérir le mutex avec try_lock(). Si le mutex est déjà détenu par un autre thread, la construction réussit quand même mais le verrou n'est pas acquis :
std::unique_lock lock(mtx, std::try_to_lock);
if (lock.owns_lock()) {
// Le mutex a été acquis — on peut travailler
process_shared_data();
} else {
// Le mutex était occupé — faire autre chose
do_alternative_work();
}| Tag | Effet | owns_lock() après construction |
|---|---|---|
| (aucun) | lock() immédiat |
true |
std::defer_lock |
Pas de verrouillage | false |
std::adopt_lock |
Adopte un verrou existant | true |
std::try_to_lock |
try_lock() non-bloquant |
true ou false |
Une fois construit, std::unique_lock expose une interface complète pour manipuler le verrou :
std::unique_lock lock(mtx, std::defer_lock);
lock.lock(); // Acquiert le mutex (bloquant)
// ... section critique ...
lock.unlock(); // Libère le mutex
// ... travail local ...
lock.lock(); // Re-acquisition possibleAppeler lock() sur un unique_lock qui détient déjà le verrou lève std::system_error. De même, appeler unlock() sans détenir le verrou est une erreur. Le booléen interne assure cette vérification.
std::unique_lock lock(mtx, std::defer_lock);
if (lock.try_lock()) {
// Acquis
} else {
// Non acquis, pas de blocage
}Quand le unique_lock encapsule un std::timed_mutex, les tentatives avec timeout sont disponibles :
#include <mutex>
#include <chrono>
std::timed_mutex tmtx;
void timed_operation() {
using namespace std::chrono_literals;
std::unique_lock lock(tmtx, std::defer_lock);
// Attente avec durée maximale
if (lock.try_lock_for(200ms)) {
// Acquis dans les 200ms
process();
} else {
// Timeout — le mutex n'a pas été obtenu
handle_timeout();
}
// Ou avec un instant absolu
auto deadline = std::chrono::steady_clock::now() + 1s;
if (lock.try_lock_until(deadline)) {
// Acquis avant le deadline
}
}std::unique_lock fournit des méthodes pour interroger son état à tout moment :
std::unique_lock lock(mtx, std::defer_lock);
lock.owns_lock(); // false — le mutex n'est pas détenu
// Équivalent :
if (lock) { // Conversion implicite en bool
// mutex détenu
}
lock.mutex(); // Retourne un pointeur vers le mutex associé (std::mutex*)La conversion en bool est idiomatique et rend le code très lisible, surtout combinée avec try_to_lock :
if (std::unique_lock lock(mtx, std::try_to_lock); lock) {
// C++17 : if avec initialiseur
// Le mutex est acquis, et 'lock' n'existe que dans ce scope
update_shared_data();
}
// lock est détruit ici → mutex libéré (s'il était acquis)Contrairement à lock_guard, un unique_lock est déplaçable (mais pas copiable). Cela permet de transférer la propriété d'un verrou entre scopes, fonctions, ou conteneurs :
#include <mutex>
std::mutex mtx;
std::unique_lock<std::mutex> acquire_lock() {
std::unique_lock lock(mtx);
// ... éventuellement vérifier un état ...
return lock; // Déplacement implicite (RVO ou move)
}
void caller() {
auto lock = acquire_lock();
// Le mutex est verrouillé ici — la propriété a été transférée
modify_shared_state();
} // lock détruit → mutex libéréCe pattern est utile quand la logique d'acquisition du verrou est complexe (vérification de préconditions, choix entre plusieurs mutex, retry avec backoff) et que vous voulez l'encapsuler dans une fonction dédiée.
Après un déplacement, l'objet source est dans un état « vide » — il n'est associé à aucun mutex :
std::unique_lock lock1(mtx);
std::unique_lock lock2 = std::move(lock1);
// lock1 n'est plus associé à aucun mutex
// lock1.mutex() == nullptr
// lock1.owns_lock() == false
// lock2 détient le verrou sur mtx
// lock2.owns_lock() == trueL'un des usages les plus importants de std::defer_lock avec std::unique_lock est le verrouillage simultané de plusieurs mutex, en évitant les deadlocks. C'est le pattern standard pré-C++17 :
#include <mutex>
#include <print>
struct Account {
std::mutex mtx;
double balance;
std::string name;
};
void transfer(Account& from, Account& to, double amount) {
// 1. Créer les locks sans verrouiller
std::unique_lock lock_from(from.mtx, std::defer_lock);
std::unique_lock lock_to(to.mtx, std::defer_lock);
// 2. Verrouiller les deux simultanément (évitement de deadlock)
std::lock(lock_from, lock_to);
// 3. Les deux mutex sont verrouillés — opération sûre
if (from.balance >= amount) {
from.balance -= amount;
to.balance += amount;
std::println("Transfert de {:.2f} : {} → {}", amount, from.name, to.name);
}
}
// 4. Destruction : les deux mutex sont libérés automatiquementstd::lock() utilise un algorithme d'évitement de deadlock (typiquement try-and-back-off) pour acquérir les deux mutex sans risque de blocage circulaire, quel que soit l'ordre dans lequel transfer(a, b, 100) et transfer(b, a, 50) sont appelés simultanément par des threads différents.
💡 Depuis C++17,
std::scoped_lock(section 21.2.4) remplace ce pattern en une seule ligne. Mais le patterndefer_lock+std::lock()reste pertinent quand vous avez besoin de la flexibilité deunique_lockaprès l'acquisition (déverrouillage anticipé, condition variables, etc.).
C'est le cas d'usage qui rend std::unique_lock indispensable — et qu'aucun autre wrapper ne peut remplir. std::condition_variable::wait() prend exclusivement un std::unique_lock<std::mutex> :
#include <mutex>
#include <condition_variable>
#include <queue>
#include <print>
std::mutex mtx;
std::condition_variable cv;
std::queue<int> tasks;
bool finished = false;
void producer() {
for (int i = 0; i < 10; ++i) {
{
std::lock_guard lock(mtx); // lock_guard suffit ici
tasks.push(i);
}
cv.notify_one(); // Réveiller un consommateur
}
{
std::lock_guard lock(mtx);
finished = true;
}
cv.notify_all();
}
void consumer() {
while (true) {
std::unique_lock lock(mtx); // unique_lock obligatoire pour wait()
// wait() fait :
// 1. Déverrouiller le mutex (lock.unlock())
// 2. Suspendre le thread
// 3. Au réveil : re-verrouiller le mutex (lock.lock())
// 4. Vérifier le prédicat — si faux, retour à l'étape 1
cv.wait(lock, [] {
return !tasks.empty() || finished;
});
if (tasks.empty() && finished) {
break; // Plus rien à traiter
}
int task = tasks.front();
tasks.pop();
lock.unlock(); // Libérer le mutex AVANT le traitement
std::println("Traitement de la tâche {}", task);
}
}Pourquoi std::condition_variable exige-t-elle std::unique_lock et non std::lock_guard ? Parce que wait() doit déverrouiller le mutex pendant l'attente (pour laisser d'autres threads modifier la condition), puis le re-verrouiller au réveil. Ces opérations unlock()/lock() intermédiaires sont impossibles avec lock_guard, dont l'interface ne les expose pas.
📎 Les variables de condition sont couvertes en profondeur dans la section 21.3, avec les patterns producteur/consommateur, les spurious wakeups, et les bonnes pratiques.
La méthode release() dissocie le unique_lock du mutex sans le déverrouiller. Elle retourne un pointeur vers le mutex, et vous devenez responsable de l'appel à unlock() :
std::unique_lock lock(mtx);
// ... section critique ...
std::mutex* raw = lock.release();
// lock n'est plus associé à aucun mutex
// Le mutex est TOUJOURS verrouillé — c'est votre responsabilité
// ... utilisation avancée ...
raw->unlock(); // Déverrouillage manuel obligatoireCe cas est rare et réservé à des scénarios d'interopérabilité avec du code C ou des API qui gèrent elles-mêmes le cycle de vie du verrou. Dans le code courant, vous n'aurez jamais besoin de release().
Un piège fréquent est de créer un unique_lock temporaire qui est immédiatement détruit :
std::mutex mtx;
int shared_data = 0;
void broken() {
// ❌ Le unique_lock est un temporaire anonyme.
// Il est créé, verrouille le mutex, puis est immédiatement détruit
// → le mutex est déverrouillé AVANT l'accès à shared_data.
std::unique_lock<std::mutex>(mtx);
shared_data++; // Data race ! Le mutex n'est plus détenu.
}
void correct() {
// ✅ Le unique_lock est nommé — il vit jusqu'à la fin du scope
std::unique_lock lock(mtx);
shared_data++; // Protégé par le mutex
}Ce piège existe aussi avec lock_guard et scoped_lock. La règle est simple : donnez toujours un nom à vos verrous.
std::unique_lock est légèrement plus lourd que lock_guard en raison de son état interne :
- Un pointeur vers le mutex (comme
lock_guard). - Un
boolindiquant si le mutex est actuellement détenu (owns_lock()).
Chaque appel à lock(), unlock(), try_lock(), etc. met à jour ce booléen. Le destructeur vérifie le booléen avant d'appeler unlock().
En pratique, ce surcoût est négligeable — de l'ordre d'un test de branche supplémentaire. Le compilateur optimise bien ce pattern, et le coût du mutex lui-même (acquisition atomique, potentiel appel système) domine largement.
Néanmoins, si vous n'avez besoin d'aucune flexibilité, lock_guard ou scoped_lock expriment mieux votre intention et éliminent toute question sur les performances.
| Aspect | Détail |
|---|---|
| Header | <mutex> |
| Construction | Verrouillage immédiat, différé (defer_lock), par adoption (adopt_lock), ou tentative (try_to_lock) |
| Destruction | unlock() si le mutex est détenu |
| Déverrouillage anticipé | Oui — lock.unlock() |
| Re-verrouillage | Oui — lock.lock() |
| Timeout | Oui — try_lock_for(), try_lock_until() (avec timed_mutex) |
| Déplaçable | Oui |
| Copiable | Non |
Compatible condition_variable |
Oui — seul wrapper compatible |
| Multi-mutex | Via std::lock() + defer_lock |
Surcoût vs lock_guard |
Minimal (un bool + une branche dans le destructeur) |
Utilisez std::unique_lock quand vous avez besoin d'au moins une de ces capacités :
- Déverrouillage anticipé avant la fin du scope.
- Utilisation avec
std::condition_variable. - Verrouillage différé ou tentative de verrouillage.
- Transfert de propriété du verrou (déplacement).
- Verrouillage avec timeout.
Dans tous les autres cas, préférez std::lock_guard ou std::scoped_lock pour leur simplicité.
À suivre : la section 21.2.4 présente
std::scoped_lock(C++17), qui combine la simplicité delock_guardavec la capacité de verrouiller plusieurs mutex simultanément sans risque de deadlock.