Mutex, verrous et sections critiques
Maîtrisez les primitives de synchronisation : mutex, lock_guard, unique_lock, scoped_lock, shared_mutex. Apprenez à protéger les données partagées, à éviter les interblocages et à distinguer data race et condition de concurrence.
Pourquoi la synchronisation est importante
Lorsque plusieurs threads accèdent à la même mémoire et qu'au moins l'un d'eux écrit, vous avez une data race — une forme de comportement indéfini en C++. La norme ne se contente pas de dire « le résultat est imprévisible ». Elle dit que *tout peut arriver* : données corrompues, plantages, failles de sécurité, ou comportement apparemment correct qui casse dès que vous changez les options du compilateur.
Un mutex (exclusion mutuelle) est la primitive de synchronisation fondamentale. Un seul thread à la fois peut détenir le verrou d'un mutex. Tout autre thread qui tente de le verrouiller se bloque jusqu'à ce que le mutex soit libéré. En protégeant les données partagées avec un mutex, vous sérialisez les accès et éliminez les data races.
std::mutex et lock_guard
Le modèle le plus simple et le plus sûr est std::lock_guard — un wrapper RAII qui verrouille un mutex à sa construction et le déverrouille à sa destruction, même si une exception est levée.
#include <iostream>
#include <thread>
#include <mutex>
#include <vector>
std::mutex mtx;
int shared_counter = 0;
void increment(int times) {
for (int i = 0; i < times; ++i) {
std::lock_guard<std::mutex> lock(mtx); // RAII lock
++shared_counter; // safe: only one thread at a time
} // lock released here automatically
}
int main() {
std::vector<std::thread> threads;
for (int i = 0; i < 10; ++i) {
threads.emplace_back(increment, 10000);
}
for (auto& t : threads) {
t.join();
}
std::cout << "Counter: " << shared_counter << "\n";
// Always prints 100000 — no data race
return 0;
}unique_lock — un verrouillage flexible
std::unique_lock est plus flexible que lock_guard. Il permet le verrouillage différé, le verrouillage avec délai, le verrouillage/déverrouillage manuel, et il est requis par std::condition_variable.
#include <iostream>
#include <thread>
#include <mutex>
#include <chrono>
std::mutex mtx;
void try_lock_example() {
// Deferred locking — don't lock yet
std::unique_lock<std::mutex> lock(mtx, std::defer_lock);
// Try to lock with a timeout
if (lock.try_lock_for(std::chrono::milliseconds(100))) {
std::cout << "Acquired lock\n";
// ... do work ...
} else {
std::cout << "Could not acquire lock within 100ms\n";
}
// Automatically unlocks when lock goes out of scope
}
void manual_control() {
std::unique_lock<std::mutex> lock(mtx);
// ... critical section ...
lock.unlock(); // release early if needed
// ... non-critical work ...
lock.lock(); // re-acquire
// ... another critical section ...
}scoped_lock — verrouiller plusieurs mutex sans interblocage (C++17)
Lorsque vous devez verrouiller plusieurs mutex simultanément, std::scoped_lock (C++17) utilise en interne un algorithme d'évitement des interblocages. Il remplace l'usage manuel de std::lock, source d'erreurs.
#include <mutex>
#include <thread>
#include <iostream>
struct BankAccount {
std::mutex mtx;
double balance = 1000.0;
};
void transfer(BankAccount& from, BankAccount& to, double amount) {
// Lock both mutexes without risk of deadlock
std::scoped_lock lock(from.mtx, to.mtx);
if (from.balance >= amount) {
from.balance -= amount;
to.balance += amount;
std::cout << "Transferred " << amount << "\n";
}
} // both mutexes released
int main() {
BankAccount alice, bob;
// These two transfers lock accounts in different order,
// but scoped_lock prevents deadlock.
std::thread t1(transfer, std::ref(alice), std::ref(bob), 100.0);
std::thread t2(transfer, std::ref(bob), std::ref(alice), 50.0);
t1.join();
t2.join();
std::cout << "Alice: " << alice.balance
<< ", Bob: " << bob.balance << "\n";
return 0;
}shared_mutex — verrou lecteurs-rédacteur (C++17)
Lorsque les lectures sont bien plus nombreuses que les écritures, std::shared_mutex autorise plusieurs lecteurs simultanés mais un accès exclusif pour l'écriture. Utilisez std::shared_lock pour les lecteurs et std::unique_lock pour les rédacteurs.
#include <shared_mutex>
#include <mutex>
#include <thread>
#include <iostream>
#include <map>
#include <string>
class ThreadSafeCache {
mutable std::shared_mutex smtx_;
std::map<std::string, int> data_;
public:
int read(const std::string& key) const {
std::shared_lock lock(smtx_); // multiple readers OK
auto it = data_.find(key);
return it != data_.end() ? it->second : -1;
}
void write(const std::string& key, int value) {
std::unique_lock lock(smtx_); // exclusive access
data_[key] = value;
}
};Une data race se produit lorsque deux threads accèdent simultanément au même emplacement mémoire, qu'au moins l'un d'eux écrit, et qu'il n'y a aucune synchronisation. En C++, c'est un comportement indéfini — pas simplement une « sortie non déterministe », mais un UB où tout peut arriver.
Une condition de concurrence (race condition) est un bogue logique de plus haut niveau, où la justesse du programme dépend du minutage. Même un code exempt de data races peut comporter des conditions de concurrence (par exemple, un « vérifier puis agir » sur une map concurrente). Les mutex éliminent les data races ; éliminer les conditions de concurrence exige une conception soignée de vos algorithmes concurrents.
Utilisez toujours des wrappers de verrou RAII (lock_guard, scoped_lock, unique_lock) plutôt que des appels bruts à mutex::lock()/unlock(). Gardez les sections critiques aussi courtes que possible — si vous pouvez l'éviter, n'effectuez jamais d'E/S, d'allocations ou d'appels bloquants en détenant un verrou. Lorsque vous avez besoin de plusieurs mutex, utilisez std::scoped_lock pour éviter les interblocages. Si vous devez verrouiller manuellement, verrouillez toujours dans un ordre global cohérent.
- Une data race est un comportement indéfini — utilisez un mutex pour protéger chaque variable modifiable partagée
std::lock_guardest le verrou RAII le plus simple ; préférez-le pour les sections critiques sans subtilitéstd::unique_lockapporte de la flexibilité : verrouillage différé, verrouillage avec délai et verrouillage/déverrouillage manuelstd::scoped_lock(C++17) verrouille plusieurs mutex simultanément sans interblocagestd::shared_mutex(C++17) autorise plusieurs lecteurs simultanés mais des rédacteurs exclusifs- Gardez les sections critiques courtes et ne détenez jamais un verrou pendant que vous attendez un autre thread
Quiz — Testez vos connaissances
(20 XP)1. Quel est le principal avantage de `std::scoped_lock` par rapport à l'appel manuel de `lock()` sur plusieurs mutex ?
2. Quel type de verrou faut-il utiliser avec `std::condition_variable` ?
3. En C++, quelle est la conséquence d'une data race ?