सामग्री पर जाएं

कंडीशन वेरिएबल और प्रोड्यूसर-कंज़्यूमर

थ्रेड के बीच कुशलता से तालमेल बिठाने के लिए कंडीशन वेरिएबल का उपयोग करें। थ्रेड-सेफ़ प्रोड्यूसर-कंज़्यूमर क्यू बनाएँ, स्प्यूरियस वेकअप सँभालें, और लॉस्ट वेकअप की समस्या से बचें।

कंडीशन वेरिएबल से थ्रेड का तालमेल

कंडीशन वेरिएबल (condition variable) किसी थ्रेड को बिना busy-loop चलाए तब तक इंतज़ार (wait) करने देता है जब तक कोई शर्त सही न हो जाए। while (!ready) { /* burn CPU */ } की तरह घूमते रहने की बजाय, थ्रेड सो जाता है और उसे कोई दूसरा थ्रेड notify_one() या notify_all() कॉल करके जगाता है।

std::condition_variable केवल std::unique_lock के साथ काम करता है। पैटर्न हमेशा यही रहता है:

1. म्यूटेक्स को लॉक करें।
2. शर्त जाँचें — अगर वह false है, तो wait() कॉल करें, जो परमाणु रूप से (atomically) म्यूटेक्स को रिलीज़ करता है और थ्रेड को सस्पेंड कर देता है।
3. नोटिफ़िकेशन मिलने पर (और शर्त सही होने पर), म्यूटेक्स दोबारा हासिल किया जाता है और निष्पादन आगे बढ़ता है।

नोटिफ़ाई करने वाला थ्रेड उसी म्यूटेक्स का लॉक पकड़कर साझा स्टेट बदलता है, और फिर notify_one() या notify_all() कॉल करता है।

कंडीशन वेरिएबल का बुनियादी उपयोग

यह बुनियादी पैटर्न है: एक थ्रेड डेटा का इंतज़ार करता है, दूसरा उसे उपलब्ध कराता है।

condition_variable_basic.cpp
#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;
}
सामान्य गलती

कंडीशन वेरिएबल किसी थ्रेड को तब भी जगा सकता है जब कोई नोटिफ़िकेशन भेजा ही नहीं गया — इसे स्प्यूरियस वेकअप (spurious wakeup) कहते हैं। प्रदर्शन के कारणों से 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!

बिना प्रेडिकेट या आसपास के while लूप के cv.wait(lock) कभी कॉल न करें।

थ्रेड-सेफ़ बाउंडेड बफ़र (प्रोड्यूसर-कंज़्यूमर)

प्रोड्यूसर-कंज़्यूमर (producer-consumer) पैटर्न सबसे आम कंकरेंसी पैटर्न में से एक है। प्रोड्यूसर साझा बफ़र में आइटम जोड़ते हैं; कंज़्यूमर उन्हें हटाते हैं। बाउंडेड बफ़र भरा होने पर प्रोड्यूसर को और ख़ाली होने पर कंज़्यूमर को ब्लॉक कर देता है।

bounded_queue.cpp
#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;
}

लॉस्ट वेकअप की समस्या

लॉस्ट वेकअप (lost wakeup) तब होता है जब notify_one() को कंज़्यूमर के wait() कॉल करने से पहले कॉल कर दिया जाए। चूँकि कोई थ्रेड इंतज़ार नहीं कर रहा होता, नोटिफ़िकेशन खो जाता है — और कंज़्यूमर फिर हमेशा के लिए सोता रहता है।

मानक पैटर्न इसे रोकता है: वह सोने से पहले प्रेडिकेट की जाँच करता है। अगर प्रोड्यूसर पहले ही शर्त को true कर चुका है, तो प्रेडिकेट true लौटाता है और wait() बिना सोए तुरंत लौट आता है। इसीलिए प्रेडिकेट वाला रूप cv.wait(lock, pred) ज़रूरी है — यह हर वेकअप से पहले और बाद में शर्त की जाँच करता है:

1. म्यूटेक्स को लॉक करें।
2. प्रेडिकेट जाँचें — अगर वह पहले से true है, तो इंतज़ार छोड़ दें।
3. अन्यथा, परमाणु रूप से अनलॉक करें और सो जाएँ।
4. जागने पर, दोबारा लॉक करें और प्रेडिकेट फिर से जाँचें।

जब तक प्रोड्यूसर और कंज़्यूमर दोनों साझा फ़्लैग की सुरक्षा उसी म्यूटेक्स से करते हैं, कोई वेकअप खोया नहीं जा सकता।

notify_one बनाम notify_all

notify_one() इंतज़ार कर रहे एक थ्रेड को जगाता है; notify_all() सभी को जगाता है। जब कई थ्रेड को अपनी शर्तें दोबारा जाँचनी पड़ सकती हों (जैसे, शटडाउन सिग्नल), तब notify_all() का उपयोग करें, और जब ठीक एक थ्रेड को आगे बढ़ना हो (जैसे, एकल कंज़्यूमर का कोई टास्क उठाना), तब notify_one() का।

notify_all_shutdown.cpp
#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 के साथ कभी इंतज़ार (wait) न करें — आपको unique_lock चाहिए।

मुख्य बातें
  • std::condition_variable थ्रेड को तब तक सोने देता है जब तक शर्त true न हो जाए, जिससे महँगी busy-waiting से बचा जा सकता है
  • स्प्यूरियस वेकअप और लॉस्ट वेकअप दोनों को सँभालने के लिए हमेशा wait() का प्रेडिकेट वाला रूप इस्तेमाल करें
  • प्रोड्यूसर-कंज़्यूमर पैटर्न दो कंडीशन वेरिएबल का उपयोग करता है: "not full" और "not empty"
  • notify_one() एक वेटर को जगाता है; notify_all() सभी वेटर को जगाता है — अपने परिदृश्य के अनुसार सही वाला चुनें
  • साझा शर्त की सुरक्षा के लिए वेटर और नोटिफ़ायर दोनों को एक ही म्यूटेक्स इस्तेमाल करना चाहिए

क्विज़ — अपना ज्ञान परखें

(20 XP)

1. आपको `condition_variable::wait()` के साथ हमेशा प्रेडिकेट का उपयोग क्यों करना चाहिए?

2. बाउंडेड प्रोड्यूसर-कंज़्यूमर क्यू में, क्यू भर जाने पर प्रोड्यूसर को क्या करना चाहिए?

3. `std::condition_variable::wait()` के लिए कौन-सा लॉक प्रकार ज़रूरी है?