1. Vấn đề nó giải quyết
Một luồng cần chờ dữ liệu, nhưng thông báo có thể đến trước khi nó bắt đầu chờ. Hãy lưu điều kiện “đã có dữ liệu” trong trạng thái được mutex bảo vệ, rồi dùng wait(lock, predicate) để kiểm tra trạng thái đó.
2. Kiến thức cần có
Biết mutex,
std::unique_lockvà data race: truy cập xung đột giữa các luồng mà thiếu đồng bộ cần thiết.Hiểu mô hình một luồng ghi dữ liệu và một luồng đọc dữ liệu.
3. Ý tưởng cốt lõi
Condition variable không lưu sự kiện của ứng dụng. Predicate là phép kiểm tra điều kiện, ví dụ ready == true, cho biết luồng có được tiếp tục hay chưa.
wait(lock, pred) tương đương vòng lặp while (!pred()) wait(lock). Khi cần chờ, wait mở khóa mutex và bắt đầu chặn như một bước nguyên tử; trước khi trả về, nó khóa mutex lại. Quy tắc chờ của C++.
4. Cú pháp tối thiểu
std::unique_lock lock(mutex);
condition.wait(lock, [&] { return ready; });
consume(payload);Đây là phần phía đọc; mutex, condition, ready, payload và consume đã được khai báo. Khóa vẫn đang được giữ khi đọc payload.
5. Cách nó hoạt động
Phía ghi giữ cùng mutex, ghi
payload, rồi đặtready = true.Phía ghi nhả khóa và gọi
notify_one(). Có thể thông báo khi còn giữ khóa, nhưng luồng đọc vẫn phải chờ lấy khóa.Nếu thông báo đến sớm, phía đọc thấy
readyđã đúng khi giữ khóa và không cần ngủ. Trạng thái được lưu giúp tránh lost wakeup: bỏ lỡ thông báo rồi chờ mãi dù công việc đã sẵn sàng.Nếu
waittự tỉnh mà không có thông báo phù hợp, đó là spurious wakeup. Predicate được kiểm tra lại; nếu sai, luồng tiếp tục chờ.Ngay cả sau một thông báo thật, với nhiều phía đọc, luồng khác có thể đã lấy mất công việc trước khi luồng này lấy khóa. Vì vậy vẫn phải kiểm tra điều kiện.
6. Lỗi thường gặp
Dùng
if (!ready) wait(lock)rồi tin rằng mọi lần tỉnh đều có dữ liệu.Chỉ gọi
notify_one()mà không ghi trạng thái có thể kiểm tra lại.Thay
readythành atomic nhưng sửa nó ngoài mutex mà không thiết kế lại giao thức chờ. Atomic riêng lẻ không đóng khoảng trống giữa kiểm tra điều kiện và bắt đầu chờ.Hủy condition variable hoặc dữ liệu dùng chung khi luồng khác vẫn có thể chờ/truy cập chúng.
7. Khi nào nên dùng
Dùng condition variable khi một luồng cần ngủ cho tới khi trạng thái dùng chung cho phép tiếp tục: hàng đợi có dữ liệu, còn chỗ trống hoặc cần dừng. Với hàng đợi thực tế, predicate thường là !queue.empty() || stopping thay vì một cờ thông báo đơn lẻ.
8. Ví dụ đơn giản
Phía ghi trong mã mẫu C++20 thực hiện:
{
const std::lock_guard lock(mutex);
payload = 42;
ready = true;
}
condition.notify_one();Phía đọc dùng predicate ở trên rồi in payload. Kết quả là 42 dù luồng nào chạy trước. Hai luồng được join() trước khi dữ liệu dùng chung bị hủy.
Mã mẫu hoàn chỉnh
Tệp mã nguồn
dailycppinterview/219_condition-variable-predicates/main.cpp
// Real-World C++ Interviews Q219: Why must condition-variable waits use a predicate, and how do
// lost and spurious wakeups differ?
// Key: A condition variable carries no application state, so the protected predicate is the
// durable fact a waiter needs. Waiting atomically unlocks the associated mutex and blocks, then
// reacquires it before returning; the predicate overload rechecks in a loop because a wait may
// wake spuriously. A lost wakeup is a protocol bug in which notification occurs before a waiter
// is actually waiting and no recorded predicate preserves the event; checking and changing the
// predicate under the same mutex prevents that gap. Publish the state first, notify according
// to the protocol, and never treat notification alone as proof that the condition holds.
#include <condition_variable>
#include <iostream>
#include <mutex>
#include <thread>
int main() {
std::mutex mutex;
std::condition_variable condition;
bool ready = false;
int payload = 0;
std::thread consumer([&] {
std::unique_lock lock(mutex);
condition.wait(lock, [&] { return ready; });
std::cout << payload << std::endl;
});
std::thread producer([&] {
{
const std::lock_guard lock(mutex);
payload = 42;
ready = true;
}
condition.notify_one();
});
producer.join();
consumer.join();
}
9. Điều cần nhớ
Mutex bảo vệ trạng thái; predicate quyết định có thể tiếp tục; notification nhắc luồng chờ kiểm tra lại. Chỉ nhận được thông báo chưa đủ để kết luận điều kiện đúng.
10. Câu hỏi tự kiểm tra
Đề đầy đủ: Vì sao condition-variable wait phải dùng predicate, và lost wakeup khác spurious wakeup thế nào?
Nếu phía ghi đặt ready = true và thông báo trước khi phía đọc gọi wait, vì sao mã mẫu vẫn chạy? Nếu có lần tỉnh giả khi ready == false, vòng chờ làm gì?