Mutexes, Locks & Critical Sections
Master synchronization primitives: mutex, lock_guard, unique_lock, scoped_lock, shared_mutex. Learn to protect shared data, prevent deadlocks, and understand the difference between data races and race conditions.
Why Synchronization Matters
When multiple threads access the same memory and at least one of them writes, you have a data race — a form of undefined behavior in C++. The standard does not merely say "the result is unpredictable." It says *anything can happen*: corrupted data, crashes, security vulnerabilities, or seemingly correct behavior that breaks when you change compiler flags.
A mutex (mutual exclusion) is the fundamental synchronization primitive. Only one thread can hold a mutex's lock at a time. Any other thread that tries to lock it will block until the mutex is released. By protecting shared data with a mutex, you serialize access and eliminate data races.
std::mutex και lock_guard
Το απλούστερο και ασφαλέστερο μοτίβο είναι τοstd::lock_guard— ένα περιτύλιγμα RAII που κλειδώνει ένα mutex στην κατασκευή και το ξεκλειδώνει κατά την καταστροφή, ακόμα κι αν υπάρχει εξαίρεση.
#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 — Ευέλικτο κλείδωμα
Τοstd::unique_lockείναι πιο ευέλικτο από τοlock_guard. Υποστηρίζει αναβολή κλειδώματος, χρονισμένο κλείδωμα, χειροκίνητο κλείδωμα/ξεκλείδωμα και απαιτείται από το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 — Multi-Lock χωρίς αδιέξοδο (C++17)
Όταν χρειάζεται να κλειδώσετε πολλά mutexes ταυτόχρονα, τοstd::scoped_lock(C++17) χρησιμοποιεί εσωτερικά έναν αλγόριθμο αποφυγής αδιεξόδου. Αντικαθιστά την επιρρεπή σε σφάλματα χειροκίνητη χρήση του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 — Κλείδωμα Reader-Writer (C++17)
Όταν οι αναγνώσεις υπερβαίνουν κατά πολύ τις εγγραφές, τοstd::shared_mutexεπιτρέπει πολλαπλούς ταυτόχρονους αναγνώστες αλλά αποκλειστική πρόσβαση στο πρόγραμμα εγγραφής. Χρησιμοποιήστεstd::shared_lockγια αναγνώστες καιstd::unique_lockγια συγγραφείς.
#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;
}
};Μια κούρσα δεδομένων εμφανίζεται όταν δύο νήματα έχουν πρόσβαση στην ίδια θέση μνήμης ταυτόχρονα και τουλάχιστον ένα γράφει, χωρίς συγχρονισμό. Στο C++ αυτή είναι απροσδιόριστη συμπεριφορά — όχι απλώς "μη ντετερμινιστική έξοδος" αλλά οτιδήποτε μπορεί να συμβεί UB.
Μια συνθήκη αγώνα είναι ένα λογικό σφάλμα υψηλότερου επιπέδου όπου η ορθότητα του προγράμματος εξαρτάται από το χρονοδιάγραμμα. Ακόμη και ο κώδικας χωρίς φυλές δεδομένων μπορεί να έχει συνθήκες αγώνων (π.χ. ελέγξτε και στη συνέχεια ενεργήστε σε έναν ταυτόχρονο χάρτη). Τα Mutexe εξαλείφουν τις φυλές δεδομένων. Η εξάλειψη των συνθηκών αγώνα απαιτεί προσεκτικό σχεδιασμό των ταυτόχρονων αλγορίθμων σας.
Χρησιμοποιείτε πάντα περιτυλίγματα κλειδαριάς RAII (lock_guard,scoped_lock,unique_lock) αντί για ακατέργαστες κλήσειςmutex::lock()/unlock(). Διατηρήστε τα κρίσιμα τμήματα όσο το δυνατόν σύντομα — μην εκτελείτε ποτέ I/O, εκχωρήσεις ή αποκλείοντας κλήσεις ενώ κρατάτε ένα κλείδωμα, εάν μπορείτε να το αποφύγετε. Όταν χρειάζεστε πολλά mutexe, χρησιμοποιήστε τοstd::scoped_lockγια να αποφύγετε αδιέξοδα. Εάν πρέπει να κλειδώσετε χειροκίνητα, να κλειδώνετε πάντα με συνεπή παγκόσμια σειρά.
- Μια κούρσα δεδομένων είναι απροσδιόριστη συμπεριφορά — χρησιμοποιήστε μια mutex για να προστατεύσετε κάθε κοινόχρηστη μεταβλητή μεταβλητή
- Το
std::lock_guardείναι η απλούστερη κλειδαριά RAII. προτιμήστε το για απλές κρίσιμες ενότητες - Το
std::unique_lockπροσθέτει ευελιξία: καθυστερημένο κλείδωμα, χρονομετρημένο κλείδωμα και χειροκίνητο κλείδωμα/ξεκλείδωμα - Το
std::scoped_lock(C++17) κλειδώνει πολλαπλά mutexes ταυτόχρονα χωρίς αδιέξοδο - Το
std::shared_mutex(C++17) επιτρέπει πολλαπλούς ταυτόχρονους αναγνώστες αλλά αποκλειστικούς συγγραφείς - Κρατήστε τα κρίσιμα τμήματα σύντομα και μην κρατάτε ποτέ κλειδαριά ενώ περιμένετε σε άλλο νήμα
Κουίζ — Δοκιμάστε τις γνώσεις σας
(20 XP)1. Ποιο είναι το κύριο πλεονέκτημα του`std::scoped_lock`σε σχέση με τη μη αυτόματη κλήση του`lock()`σε πολλαπλά mutexe;
2. Which lock type should you use with`std::condition_variable`?
3. Στο C++, ποιες είναι οι συνέπειες μιας κούρσας δεδομένων;