Mutexy, zámky a kritické sekce
Hlavní synchronizační primitiva: mutex, lock_guard, unique_lock, scoped_lock, shared_mutex. Naučte se chránit sdílená data, předcházet uváznutí a pochopit rozdíl mezi závody dat a podmínkami závodu.
Proč na synchronizaci záleží
Když více vláken přistupuje ke stejné paměti a alespoň jedno z nich zapisuje, máte datový závod – formu nedefinovaného chování v C++. Norma neříká pouze „výsledek je nepředvídatelný“. Říká, že *se může stát cokoliv*: poškozená data, pády, zranitelnosti zabezpečení nebo zdánlivě správné chování, které se přeruší, když změníte příznaky kompilátoru.
mutex (vzájemné vyloučení) je základním synchronizačním primitivem. Zámek mutexu může mít vždy pouze jedno vlákno. Jakékoli jiné vlákno, které se jej pokusí uzamknout, se zablokuje, dokud nebude mutex uvolněn. Ochranou sdílených dat pomocí mutexu serializujete přístup a eliminujete závody dat.
std::mutex a lock_guard
Nejjednodušším a nejbezpečnějším vzorem jestd::lock_guard— obal RAII, který uzamkne mutex na konstrukci a odemkne jej při zničení, i když je vyvolána výjimka.
#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 — Flexibilní zamykání
std::unique_lockje flexibilnější nežlock_guard. Podporuje odložené zamykání, časované zamykání, ruční zamykání/odemykání a je vyžadovánostd::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 — Multi-Lock bez uváznutí (C++17)
Když potřebujete uzamknout více mutexů současně,std::scoped_lock(C++17) používá interně algoritmus pro zamezení uváznutí. Nahrazuje ruční použitístd::locknáchylné k chybám.
#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 — Reader-Writer Lock (C++17)
Když čtení výrazně převyšuje počet zápisů,std::shared_mutexumožňuje více souběžných čtenářů, ale exkluzivní přístup pro zapisovače. Použijtestd::shared_lockpro čtečky astd::unique_lockpro zapisovače.
#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;
}
};datový závod nastane, když dvě vlákna přistupují ke stejnému paměťovému umístění současně a alespoň jedno zapisuje bez synchronizace. V C++ jde o nedefinované chování — nejen „nedeterministický výstup“, ale UB, co se může stát.
Race condition je logická chyba vyšší úrovně, kde správnost programu závisí na načasování. Dokonce i kód bez datových závodů může mít podmínky závodu (např. kontrola a poté akce na souběžné mapě). Mutexy eliminují datové závody; eliminace rasových podmínek vyžaduje pečlivý návrh vašich souběžných algoritmů.
Vždy používejte RAII lock wrappery (lock_guard,scoped_lock,unique_lock) namísto nezpracovaných volánímutex::lock()/unlock(). Udržujte kritické sekce co kratší – nikdy neprovádějte I/O, přidělování nebo blokování hovorů, když držíte zámek, pokud se tomu můžete vyhnout. Pokud potřebujete více mutexů, použijtestd::scoped_lock, abyste se vyhnuli uváznutí. Pokud musíte uzamknout ručně, vždy zamykejte v konzistentním globálním pořadí.
- Závod dat je nedefinované chování – použijte mutex k ochraně každé sdílené měnitelné proměnné
std::lock_guardje nejjednodušší RAII zámek; preferujte jej pro přímočaré kritické sekcestd::unique_lockpřidává flexibilitu: odložené zamykání, časované zamykání a ruční zamykání/odemykánístd::scoped_lock(C++17) uzamkne více mutexů současně bez uváznutístd::shared_mutex(C++17) umožňuje více souběžných čtenářů, ale exkluzivní zapisovače- Udržujte kritické části krátké a nikdy nedržte zámek při čekání na další vlákno
Kvíz — Otestujte své znalosti
(20 XP)1. Jaká je hlavní výhoda`std::scoped_lock`oproti ručnímu volání`lock()`na více mutexech?
2. Jaký typ zámku byste měli použít s`std::condition_variable`?
3. Jaký je důsledek datového závodu v C++?