متغيرات الحالة والمنتج والمستهلك
استخدم متغيرات الحالة لتنسيق الخيوط بكفاءة. أنشئ قائمة انتظار آمنة للمنتج والمستهلك، وتعامل مع عمليات التنبيه الزائفة، وتجنب مشكلة التنبيه المفقودة.
تنسيق الموضوع مع متغيرات الحالة
متغير الشرط يتيح للخيط الانتظار حتى تصبح بعض الشروط صحيحة، دون تكرار التكرار. بدلاً من تدويرwhile (!ready) { /* burn CPU */ }، ينام الخيط ويتم إيقاظه بواسطة خيط آخر يستدعيnotify_one()أوnotify_all().
يعملstd::condition_variableفقط معstd::unique_lock. النمط هو دائما:
1. قفل كائن المزامنة (mutex).
2. تحقق من الحالة - إذا كانت خاطئة، فاتصل بـwait()الذي يطلق كائن المزامنة (mutex) ذريًا ويعلق الخيط.
3. عند الإخطار (ويكون الشرط صحيحًا)، تتم إعادة الحصول على كائن المزامنة (mutex) ويستمر التنفيذ.
يقوم مؤشر ترابط الإعلام بتعديل الحالة المشتركة ضمن كائن المزامنة نفسه، ثم يستدعيnotify_one()أوnotify_all().
الحالة الأساسية الاستخدام المتغير
هذا هو النمط الأساسي: أحد الخيوط ينتظر البيانات، وآخر يقدمها.
#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;
}قد يقوم المتغير الشرطي بتنبيه سلسلة رسائل حتى في حالة عدم إرسال أي إشعار — وهذا ما يسمى التنبيه الزائف. إنه مسموح به بواسطة معيار C++ (والمشترك في الأنظمة الحقيقية) لأسباب تتعلق بالأداء. يجب عليك دائمًا استخدام النموذج الأصلي لـwait()أو لفه في حلقةwhile:
// 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 حلقة.
المخزن المؤقت المحدود الآمن للخيط (المنتج-المستهلك)
يعد نمط المنتج والمستهلك أحد أنماط التزامن الأكثر شيوعًا. يضيف المنتجون عناصر إلى المخزن المؤقت المشترك؛ المستهلكين إزالتها. ويمنع الحاجز المحدود المنتجين عندما يكونون ممتلئين والمستهلكين عندما يكونون فارغين.
#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;
}مشكلة الاستيقاظ المفقودة
يحدث التنبيه المفقود عندما يتم استدعاءnotify_one()قبل أن يتصل المستهلك بـwait(). نظرًا لعدم وجود أي سلسلة محادثات، يتم فقدان الإشعار - ثم ينام المستهلك إلى الأبد.
يمنع النمط القياسي ذلك عن طريق التحقق من المسند قبل النوم. إذا قام المنتج بالفعل بتعيين الشرط على true، فسيُرجع المسند صحيحًا ويعودwait()على الفور دون النوم. ولهذا السبب يعد النموذج المسندcv.wait(lock, pred)ضروريًا - فهو يتحقق من الحالة قبل وبعد كل تنبيه:
1. قفل كائن المزامنة (mutex).
2. تحقق من المسند — إذا كان صحيحًا بالفعل، فتخطي الانتظار.
3. بخلاف ذلك، قم بالفتح والنوم ذريًا.
4. عند التنبيه، أعد القفل وأعد التحقق من المسند.
طالما أن المنتج والمستهلك يحميان العلامة المشتركة باستخدام كائن المزامنة نفسه، فلا يمكن فقدان أي تنبيه.
notify_one مقابل notify_all
ينشطnotify_one()خيط انتظار واحد؛ يستيقظnotify_all()جميعهم. استخدمnotify_all()عندما تحتاج سلاسل رسائل متعددة إلى إعادة التحقق من ظروفها (على سبيل المثال، إشارات إيقاف التشغيل)، وnotify_one()عندما يجب متابعة مؤشر ترابط واحد بالضبط (على سبيل المثال، يلتقط مستهلك واحد مهمة).
#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;
}استخدم دائمًا التحميل الزائد المسند لـwait()للحماية من عمليات التنبيه الزائفة وعمليات التنبيه المفقودة. قم بإلغاء تأمين كائن المزامنة قبل استدعاءnotify_one()/notify_all()عندما يكون ذلك ممكنًا - وهذا يتجنب حظر الخيط المستيقظ على كائن المزامنة على الفور. تفضلnotify_one()عندما يحتاج مؤشر ترابط واحد فقط إلى التنبيه، وnotify_all()لسيناريوهات البث مثل إيقاف التشغيل. لا تنتظر مطلقًا متغير شرط معlock_guard— فأنت بحاجة إلىunique_lock.
- يتيح
std::condition_variableللخيوط السكون حتى تتحقق الحالة، مما يتجنب الانتظار المزدحم الباهظ الثمن - استخدم دائمًا النموذج الأصلي لـ
wait()للتعامل مع كل من عمليات التنبيه الزائفة وعمليات التنبيه المفقودة - يستخدم نمط المنتج والمستهلك متغيرين شرطيين: "غير ممتلئ" و"غير فارغ".
notify_one()wakes one waiter;notify_all()wakes all waiters — use the right one for your scenario- Both the waiter and the notifier must use the same mutex to protect the shared condition
اختبار — اختبر معرفتك
(20 نقطة خبرة)1. لماذا يجب عليك دائمًا استخدام المسند مع`condition_variable::wait()`؟
2. في قائمة الانتظار المحدودة بين المنتج والمستهلك، ما الذي يجب على المنتج فعله عندما تكون قائمة الانتظار ممتلئة؟
3. ما نوع القفل الذي يتطلبه`std::condition_variable::wait()`؟