Mutexe, Locks & kritische Abschnitte
Beherrsche die Synchronisationsprimitive: mutex, lock_guard, unique_lock, scoped_lock, shared_mutex. Lerne, gemeinsam genutzte Daten zu schützen, Deadlocks zu vermeiden und den Unterschied zwischen Data Races und Race Conditions zu verstehen.
Warum Synchronisation wichtig ist
Wenn mehrere Threads auf denselben Speicher zugreifen und mindestens einer davon schreibt, liegt ein Data Race vor — eine Form von undefiniertem Verhalten in C++. Der Standard sagt nicht bloß "das Ergebnis ist unvorhersehbar". Er sagt, dass *alles* passieren kann: beschädigte Daten, Abstürze, Sicherheitslücken oder scheinbar korrektes Verhalten, das kippt, sobald du die Compiler-Flags änderst.
Ein Mutex (mutual exclusion, gegenseitiger Ausschluss) ist das grundlegende Synchronisationsprimitiv. Nur ein Thread kann die Sperre eines Mutex gleichzeitig halten. Jeder andere Thread, der ihn zu sperren versucht, blockiert, bis der Mutex freigegeben wird. Indem du gemeinsam genutzte Daten mit einem Mutex schützt, serialisierst du die Zugriffe und beseitigst Data Races.
std::mutex und lock_guard
Das einfachste und sicherste Muster ist std::lock_guard — ein RAII-Wrapper, der einen Mutex bei der Konstruktion sperrt und bei der Zerstörung wieder freigibt, selbst wenn eine Exception geworfen wird.
#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 — flexibles Sperren
std::unique_lock ist flexibler als lock_guard. Er unterstützt verzögertes Sperren, Sperren mit Zeitlimit sowie manuelles lock/unlock und wird von std::condition_variable vorausgesetzt.
#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 — deadlockfreies Mehrfachsperren (C++17)
Wenn du mehrere Mutexe gleichzeitig sperren musst, verwendet std::scoped_lock (C++17) intern einen Algorithmus zur Deadlock-Vermeidung. Es ersetzt den fehleranfälligen manuellen Einsatz von std::lock.
#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 — Leser-Schreiber-Sperre (C++17)
Wenn es deutlich mehr Lese- als Schreibzugriffe gibt, erlaubt std::shared_mutex mehrere gleichzeitige Leser, aber exklusiven Zugriff für Schreiber. Verwende std::shared_lock für Leser und std::unique_lock für Schreiber.
#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;
}
};Ein Data Race entsteht, wenn zwei Threads ohne Synchronisation nebenläufig auf dieselbe Speicherstelle zugreifen und mindestens einer schreibt. In C++ ist das undefiniertes Verhalten — nicht bloß "nichtdeterministische Ausgabe", sondern UB, bei dem alles passieren kann.
Eine Race Condition (Wettlaufsituation) ist ein Logikfehler auf höherer Ebene, bei dem die Korrektheit des Programms vom Timing abhängt. Selbst Code ohne Data Races kann Race Conditions enthalten (z. B. prüfen-dann-handeln auf einer nebenläufigen Map). Mutexe beseitigen Data Races; Race Conditions zu beseitigen erfordert einen sorgfältigen Entwurf deiner nebenläufigen Algorithmen.
Verwende immer RAII-Lock-Wrapper (lock_guard, scoped_lock, unique_lock) statt roher Aufrufe von mutex::lock()/unlock(). Halte kritische Abschnitte so kurz wie möglich — führe nach Möglichkeit keine I/O, keine Allokationen und keine blockierenden Aufrufe aus, während du eine Sperre hältst. Brauchst du mehrere Mutexe, nutze std::scoped_lock, um Deadlocks zu vermeiden. Musst du doch manuell sperren, sperre immer in einer einheitlichen globalen Reihenfolge.
- Ein Data Race ist undefiniertes Verhalten — schütze jede gemeinsam genutzte veränderliche Variable mit einem Mutex
std::lock_guardist die einfachste RAII-Sperre; bevorzuge sie für geradlinige kritische Abschnittestd::unique_lockbringt mehr Flexibilität: verzögertes Sperren, Sperren mit Zeitlimit und manuelles lock/unlockstd::scoped_lock(C++17) sperrt mehrere Mutexe gleichzeitig ohne Deadlockstd::shared_mutex(C++17) erlaubt mehrere gleichzeitige Leser, aber exklusive Schreiber- Halte kritische Abschnitte kurz und halte niemals eine Sperre, während du auf einen anderen Thread wartest
Quiz — Teste dein Wissen
(20 XP)1. Was ist der wesentliche Vorteil von `std::scoped_lock` gegenüber dem manuellen Aufruf von `lock()` auf mehreren Mutexen?
2. Welchen Lock-Typ solltest du mit `std::condition_variable` verwenden?
3. Welche Konsequenz hat ein Data Race in C++?