Biến điều kiện & Nhà sản xuất-Người tiêu dùng
Sử dụng các biến điều kiện để phối hợp các luồng một cách hiệu quả. Xây dựng hàng đợi nhà sản xuất-người tiêu dùng an toàn theo luồng, xử lý các lần đánh thức giả và tránh sự cố mất đánh thức.
Phối hợp luồng với các biến điều kiện
Biến điều kiện cho phép một luồng đợi cho đến khi điều kiện nào đó trở thành đúng mà không cần lặp lại bận. Thay vì quaywhile (!ready) { /* burn CPU */ }, luồng ở chế độ ngủ và được đánh thức bởi một luồng khác gọinotify_one()hoặcnotify_all().
std::condition_variablechỉ hoạt động vớistd::unique_lock. Mẫu luôn là:
1. Khóa mutex.
2. Kiểm tra điều kiện - nếu sai, hãy gọiwait()để giải phóng nguyên tử mutex và tạm dừng luồng.
3. Khi được thông báo (và điều kiện là đúng), mutex sẽ được lấy lại và tiếp tục thực thi.
Chuỗi thông báo sửa đổi trạng thái được chia sẻ trong cùng một mutex, sau đó gọinotify_one()hoặcnotify_all().
Cách sử dụng biến điều kiện cơ bản
Đây là mẫu cơ bản: một luồng chờ dữ liệu, một luồng khác cung cấp dữ liệu đó.
#include <iostream>
#include <thread>
#include <mutex>
#include <condition_variable>
#include <string>
std::mutex mtx;
std::condition_variable cv;
std::string data;
bool ready = false;
void consumer() {
std::unique_lock<std::mutex> lock(mtx);
// Wait until ready is true (handles spurious wakeups)
cv.wait(lock, []{ return ready; });
std::cout << "Consumer received: " << data << "\n";
}
void producer() {
{
std::lock_guard<std::mutex> lock(mtx);
data = "Hello from producer";
ready = true;
} // unlock before notify to avoid waking thread into contention
cv.notify_one();
}
int main() {
std::thread t1(consumer);
std::thread t2(producer);
t1.join();
t2.join();
return 0;
}Biến điều kiện có thể đánh thức một chuỗi ngay cả khi không có thông báo nào được gửi — đây được gọi là đánh thức giả. Nó được cho phép theo tiêu chuẩn C++ (và phổ biến trên các hệ thống thực) vì lý do hiệu suất. Bạn phải luôn sử dụng dạng vị ngữ củawait()hoặc gói nó trong vòng lặpwhile:
Vòng lặp ```cpp
// CORRECT — predicate form handles spurious wakeups
cv.wait(lock, []{ return ready; });
// EQUIVALENT manual loop
while (!ready) {
cv.wait(lock);
}
// WRONG — wakes up randomly, proceeds without data
cv.wait(lock); // no condition check!
```
Never call cv.wait(lock) without a predicate or a surrounding while.
Bộ đệm giới hạn an toàn theo chủ đề (Nhà sản xuất-Người tiêu dùng)
Mẫu nhà sản xuất-người tiêu dùng là một trong những mẫu đồng thời phổ biến nhất. Nhà sản xuất thêm các mục vào bộ đệm chung; người tiêu dùng loại bỏ chúng. Bộ đệm giới hạn chặn các nhà sản xuất khi đầy và người tiêu dùng khi trống.
#include <iostream>
#include <thread>
#include <mutex>
#include <condition_variable>
#include <queue>
#include <vector>
template <typename T>
class BoundedQueue {
std::queue<T> queue_;
std::mutex mtx_;
std::condition_variable not_full_;
std::condition_variable not_empty_;
size_t capacity_;
public:
explicit BoundedQueue(size_t cap) : capacity_(cap) {}
void push(T item) {
std::unique_lock<std::mutex> lock(mtx_);
not_full_.wait(lock, [this]{ return queue_.size() < capacity_; });
queue_.push(std::move(item));
not_empty_.notify_one();
}
T pop() {
std::unique_lock<std::mutex> lock(mtx_);
not_empty_.wait(lock, [this]{ return !queue_.empty(); });
T item = std::move(queue_.front());
queue_.pop();
not_full_.notify_one();
return item;
}
};
int main() {
BoundedQueue<int> bq(5); // capacity 5
// Producer
std::thread producer([&bq]() {
for (int i = 0; i < 20; ++i) {
bq.push(i);
std::cout << "Produced: " << i << "\n";
}
});
// Consumer
std::thread consumer([&bq]() {
for (int i = 0; i < 20; ++i) {
int val = bq.pop();
std::cout << "Consumed: " << val << "\n";
}
});
producer.join();
consumer.join();
return 0;
}Vấn đề mất thức dậy
Mất đánh thức xảy ra khinotify_one()được gọi trước người tiêu dùng gọiwait(). Vì không có luồng nào đang chờ nên thông báo sẽ bị mất - người tiêu dùng sẽ ngủ vĩnh viễn.
Mẫu tiêu chuẩn ngăn chặn điều này bằng cách kiểm tra vị từ trước khi ngủ. Nếu nhà sản xuất đã đặt điều kiện thành true thì vị từ sẽ trả về true vàwait()sẽ trả về ngay lập tức mà không cần ngủ. Đây là lý do tại sao dạng vị từcv.wait(lock, pred)lại cần thiết - nó kiểm tra điều kiện trước và sau mỗi lần thức dậy:
1. Khóa mutex.
2. Kiểm tra vị từ - nếu đã đúng, bỏ qua việc chờ đợi.
3. Nếu không, hãy mở khóa nguyên tử và ngủ.
4. Khi thức dậy, khóa lại và kiểm tra lại vị ngữ.
Miễn là cả nhà sản xuất và người tiêu dùng đều bảo vệ cờ được chia sẻ bằng cùng một mutex, thì không thể đánh thức được.
thông báo_one so với thông báo_all
notify_one()đánh thức một luồng đang chờ;notify_all()đánh thức tất cả chúng. Sử dụngnotify_all()khi nhiều luồng có thể cần kiểm tra lại các điều kiện của chúng (ví dụ: tín hiệu tắt) vànotify_one()khi chính xác một luồng sẽ tiếp tục (ví dụ: một người tiêu dùng duy nhất chọn một tác vụ).
#include <iostream>
#include <thread>
#include <mutex>
#include <condition_variable>
#include <vector>
#include <atomic>
std::mutex mtx;
std::condition_variable cv;
bool shutdown = false;
void worker(int id) {
std::unique_lock<std::mutex> lock(mtx);
cv.wait(lock, []{ return shutdown; });
std::cout << "Worker " << id << " shutting down\n";
}
int main() {
std::vector<std::thread> workers;
for (int i = 0; i < 5; ++i) {
workers.emplace_back(worker, i);
}
std::this_thread::sleep_for(std::chrono::seconds(1));
{
std::lock_guard<std::mutex> lock(mtx);
shutdown = true;
}
cv.notify_all(); // wake ALL workers for shutdown
for (auto& w : workers) w.join();
return 0;
}Luôn sử dụng tình trạng quá tải vị từ củawait()để đề phòng các lần đánh thức giả và bị mất đánh thức. Mở khóa mutex trước gọinotify_one()/notify_all()khi có thể - điều này tránh chặn ngay lập tức chuỗi đánh thức trên mutex. Ưu tiênnotify_one()khi chỉ cần đánh thức một luồng vànotify_all()cho các tình huống phát sóng như tắt máy. Không bao giờ đợi biến điều kiện bằnglock_guard- bạn cầnunique_lock.
std::condition_variablecho phép các luồng ngủ cho đến khi một điều kiện là đúng, tránh tình trạng chờ đợi bận rộn tốn kém- Luôn sử dụng dạng vị ngữ của
wait()để xử lý cả đánh thức giả và đánh thức bị mất - Mẫu nhà sản xuất-người tiêu dùng sử dụng hai biến điều kiện: "không đầy" và "không trống"
notify_one()đánh thức một người phục vụ;notify_all()đánh thức tất cả người phục vụ - sử dụng đúng cho kịch bản của bạn- Cả người phục vụ và người thông báo đều phải sử dụng cùng một mutex để bảo vệ điều kiện chung
Câu đố — Kiểm tra kiến thức
(20 XP)1. Tại sao bạn phải luôn sử dụng vị từ với`condition_variable::wait()`?
2. Trong hàng đợi nhà sản xuất-người tiêu dùng bị giới hạn, nhà sản xuất nên làm gì khi hàng đợi đầy?
3. `std::condition_variable::wait()`yêu cầu loại khóa nào?