Mutexes, Khóa & Phần quan trọng
Nguyên tắc đồng bộ hóa chính: mutex, lock_guard, Unique_lock,scoped_lock, Shared_mutex. Tìm hiểu cách bảo vệ dữ liệu được chia sẻ, ngăn chặn bế tắc và hiểu sự khác biệt giữa các cuộc đua dữ liệu và điều kiện cuộc đua.
Tại sao vấn đề đồng bộ hóa
Khi nhiều luồng truy cập vào cùng một bộ nhớ và ít nhất một trong số chúng ghi, bạn sẽ gặp phải cuộc đua dữ liệu — một dạng hành vi không xác định trong C++. Tiêu chuẩn không chỉ đơn thuần nói “kết quả là không thể đoán trước”. Nó cho biết *bất cứ điều gì cũng có thể xảy ra*: dữ liệu bị hỏng, sự cố, lỗ hổng bảo mật hoặc hành vi có vẻ đúng nhưng lại bị hỏng khi bạn thay đổi cờ trình biên dịch.
mutex (loại trừ lẫn nhau) là nguyên tắc đồng bộ hóa cơ bản. Mỗi lần chỉ có một luồng có thể giữ khóa của mutex. Bất kỳ luồng nào khác cố khóa nó sẽ chặn cho đến khi mutex được giải phóng. Bằng cách bảo vệ dữ liệu được chia sẻ bằng mutex, bạn tuần tự hóa quyền truy cập và loại bỏ các cuộc đua dữ liệu.
std::mutex và lock_guard
Mẫu đơn giản và an toàn nhất làstd::lock_guard- một trình bao bọc RAII khóa một mutex khi xây dựng và mở khóa nó khi phá hủy, ngay cả khi có ngoại lệ được đưa ra.
#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 — Khóa linh hoạt
std::unique_locklinh hoạt hơnlock_guard. Nó hỗ trợ khóa hoãn lại, khóa hẹn giờ, khóa/mở khóa thủ công và đượcstd::condition_variableyêu cầu.
#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 - Đa khóa không có bế tắc (C++17)
Khi bạn cần khóa đồng thời nhiều mutexes,std::scoped_lock(C++17) sử dụng thuật toán tránh bế tắc bên trong. Nó thay thế việc sử dụngstd::lockthủ công dễ bị lỗi.
#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 - Khóa đầu đọc-ghi (C++17)
Khi số lần đọc nhiều hơn số lần ghi,std::shared_mutexcho phép nhiều trình đọc đồng thời nhưng quyền truy cập độc quyền của người ghi. Sử dụngstd::shared_lockcho người đọc vàstd::unique_lockcho người viết.
#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;
}
};Cuộc đua dữ liệu xảy ra khi hai luồng truy cập đồng thời vào cùng một vị trí bộ nhớ và ít nhất một luồng ghi mà không đồng bộ hóa. Trong C++, đây là hành vi không xác định - không chỉ đơn thuần là "đầu ra không xác định" mà là bất cứ điều gì có thể xảy ra UB.
điều kiện đua là lỗi logic cấp cao hơn trong đó tính chính xác của chương trình phụ thuộc vào thời gian. Ngay cả mã không có các cuộc đua dữ liệu cũng có thể có các điều kiện cuộc đua (ví dụ: kiểm tra rồi hành động trên bản đồ đồng thời). Mutexes loại bỏ các cuộc đua dữ liệu; việc loại bỏ các điều kiện cạnh tranh đòi hỏi phải thiết kế cẩn thận các thuật toán đồng thời của bạn.
Luôn sử dụng trình bao bọc khóa RAII (lock_guard,scoped_lock,unique_lock) thay vì lệnh gọimutex::lock()/unlock()thô. Giữ các phần quan trọng ngắn nhất có thể — không bao giờ thực hiện I/O, phân bổ hoặc chặn cuộc gọi trong khi giữ khóa nếu bạn có thể tránh được. Khi bạn cần nhiều mutex, hãy sử dụngstd::scoped_lockđể tránh bế tắc. Nếu bạn phải khóa thủ công, hãy luôn khóa theo thứ tự toàn cầu nhất quán.
- Cuộc đua dữ liệu là hành vi không xác định — sử dụng mutex để bảo vệ mọi biến có thể thay đổi được chia sẻ
std::lock_guardlà khóa RAII đơn giản nhất; thích nó cho các phần quan trọng đơn giảnstd::unique_lockbổ sung tính linh hoạt: khóa chậm, khóa hẹn giờ và khóa/mở khóa thủ côngstd::scoped_lock(C++17) khóa đồng thời nhiều mutex mà không bị bế tắcstd::shared_mutex(C++17) cho phép nhiều trình đọc đồng thời nhưng các trình ghi độc quyền- Giữ các phần quan trọng ngắn gọn và không bao giờ giữ khóa trong khi chờ đợi một chủ đề khác
Câu đố — Kiểm tra kiến thức
(20 XP)1. Ưu điểm chính của`std::scoped_lock`so với việc gọi`lock()`theo cách thủ công trên nhiều mutex là gì?
2. Which lock type should you use with`std::condition_variable`?
3. Trong C++, hậu quả của cuộc đua dữ liệu là gì?