5.2.25 · D4 · HinglishC++ Programming

Exercisesstd - condition_variable

2,569 words12 min read↑ Read in English

5.2.25 · D4 · Coding › C++ Programming › std - condition_variable

Parent: std - condition_variable · Prereqs jo open rakhna chahte ho: std::mutex, std::unique_lock, std::lock_guard, Producer-Consumer Pattern, Spurious Wakeup, Race Condition, Deadlock.


Level 1 — Recognition (kya tum pieces ka naam bata sakte ho?)

L1.1 — Kaun sa header?

std::condition_variable use karne ke liye tumhe exactly kaun sa ek header #include karna padega?

Recall Solution

==<condition_variable>==. Yeh dono std::condition_variable aur std::condition_variable_any declare karta hai. Note karo ki jis mutex ke saath yeh partner karta hai uske liye <mutex> alag se bhi chahiye.

L1.2 — wait kaun sa lock type maangta hai?

cv.wait(...) ek lock reference mein leta hai. Kya yeh std::lock_guard<std::mutex> hai ya std::unique_lock<std::mutex>? Doosra kyun nahi ho sakta?

Recall Solution

Yeh ==std::unique_lock<std::mutex>== hona chahiye. Reason: wait ko sone se pehle mutex unlock karna hota hai (taaki notifier mutex le sake aur shared data change kar sake), phir jaagne ke baad re-lock karna hota hai. Ek std::lock_guard apne constructor mein ek baar lock karta hai aur destructor mein ek baar unlock karta hai — yeh koi unlock()/lock() expose nahi karta jo mid-life call ho sake. Sirf std::unique_lock ko repeatedly unlock aur re-lock kiya ja sakta hai, joh exactly wahi hai jo wait ko chahiye.

L1.3 — notify_one vs notify_all

Ek-ek line mein batao ki notify_one() aur notify_all() kya karte hain.

Recall Solution

notify_one() sirf ek waiting thread ko jagata hai (OS decide karta hai kaun sa). notify_all() sabhi currently-waiting threads ko jagata hai; phir woh ek-ek karke mutex ke liye contend karte hain.


Level 2 — Application (chhota code sahi likho)

L2.1 — Toota hua wait fix karo

Yeh code ready == true hone tak wait karne ke liye hai. Isme do bugs hain. Dhundho aur fix karo.

std::mutex m; std::condition_variable cv; bool ready = false;
void worker() {
    std::lock_guard<std::mutex> lk(m);   // (A)
    if (!ready) cv.wait(lk);             // (B)
    // ... proceed
}
Recall Solution

Bug 1 (line A): lock_guard ko wait unlock/re-lock nahi kar sakta. Ise std::unique_lock<std::mutex> lk(m); mein badlo. Bug 2 (line B): if + plain wait(lk) ek Spurious Wakeup ya stolen wakeup se nahi bachta — thread ready ke abhi bhi false rehne par bhi jaag sakta hai aur aage badh sakta hai. Predicate loop use karo.

std::mutex m; std::condition_variable cv; bool ready = false;
void worker() {
    std::unique_lock<std::mutex> lk(m);
    cv.wait(lk, []{ return ready; });   // == while(!ready) cv.wait(lk);
    // ... proceed, mutex held
}

L2.2 — Predicate, expand karke

cv.wait(lk, []{ return !q.empty(); }); ko sirf single-argument cv.wait(lk) use karke rewrite karo, koi predicate overload nahi.

Recall Solution

Predicate overload while(!pred()) cv.wait(lk); ke roop mein define hai. Toh:

while (q.empty()) cv.wait(lk);

Dono equivalent hain; guarantee loop se aati hai, notification se nahi.

L2.3 — One-shot "go" signal

Ek main_thread() likho jo ready = true flip kare aur L2.1 ke worker ko safely jagaye (koi lost wakeup nahi).

Recall Solution
void main_thread() {
    { std::lock_guard<std::mutex> lk(m); ready = true; } // lock ke ANDAR set karo
    cv.notify_one();                                     // phir bell bajao
}

ready ko usi mutex ke under set karna jo waiter use karta hai, "waiter predicate check kare" aur "notifier flag set kare" ko serialize karta hai, jisse lost-wakeup window band ho jaati hai.


Level 3 — Analysis (behaviour aur timelines ke baare mein sochna)

L3.1 — Notify karne se pehle unlock kyun?

Producer mein humne likha:

{ std::lock_guard<std::mutex> lk(m); q.push(i); }   // yahan unlock
cv.notify_one();                                    // baad mein notify

Timeline figure ko reference karte hue explain karo ki unlock ke baad notify karna ek optimization kyun hai, aur confirm karo ki yeh correct bhi hai.

Figure — std - condition_variable
Recall Solution

Correctness: flag (q non-empty) lock ke under set hota hai, isliye koi wakeup lost nahi ho sakta — waiter, jab mutex ke under predicate dobara check karega, item dekhega. Notify order correctness ko affect nahi karta kyunki flag sachai carry karta hai. Optimization (figure dekho): agar hum mutex hold karte hue notify karein (left timeline), toh jaaga hua consumer turant m lock karne ki koshish karta hai, producer ke paas held paata hai, aur phir se block ho jaata hai — ek waste hua wake→block→wake ping-pong. Unlock ke baad notify karne par (right timeline) consumer ek free mutex par jagta hai aur seedha aage badh jaata hai. Ise kabhi kabhi "hurry up and wait" problem kehte hain, aur pehle unlock karna isse bachata hai.

L3.2 — notify_one jahan notify_all chahiye

Do consumers cv.wait(lk, []{ return !q.empty() || done; }) par so rahe hain. Producer khatam hota hai, done = true set karta hai, phir cv.notify_one() call karta hai. Kya galat ho jaata hai?

Recall Solution

Sirf ek consumer jagta hai, done dekhta hai, aur exit karta hai. Doosra consumer abhi bhi so raha hai — use ab koi dobara notify nahi karega (production khatam ho gayi), isliye yeh hamesha ke liye hang kar jaata hai (Deadlock / permanent block ki ek form). Fix: terminal done transition par cv.notify_all() use karo taaki har waiter jage, dobara check kare, done dekhe, aur exit kare.

L3.3 — Drain trace karo

Parent note ke producer-consumer code mein, producer 0,1,2,3,4 push karta hai. Ek single consumer inhe process karta hai. Consumer ke inner while (!q.empty()) loop mein, agar consumer ke jaagne par saare 5 items present hon, toh woh ek single wake mein kitni values print karega, aur unka sum kya hoga?

Recall Solution

Inner while (!q.empty()) ek hi wake mein queue ki sab kuch drain kar deta hai. Agar saare 5 present hain toh yeh 0,1,2,3,4 print karta hai — woh 5 values hain, sum .


Level 4 — Synthesis (ek bounded system assemble karo)

L4.1 — Bounded (fixed-capacity) queue

Ek plain queue bina limit ke grow karti hai. Capacity N ka ek bounded buffer design karo: producers ko bhi wait karna chahiye jab buffer full ho. Tumhe minimum kitne condition_variables chahiye, aur har waiter kaun sa predicate use karta hai?

Recall Solution

Tumhe do condition variables chahiye jo ek mutex share karein:

  • cv_not_full — producers isme wait karte hain; predicate q.size() < N.
  • cv_not_empty — consumers isme wait karte hain; predicate !q.empty().
std::mutex m;
std::condition_variable cv_not_full, cv_not_empty;
std::queue<int> q;
const size_t N = 8;
 
void produce(int x) {
    std::unique_lock<std::mutex> lk(m);
    cv_not_full.wait(lk, []{ return q.size() < N; });
    q.push(x);
    lk.unlock();
    cv_not_empty.notify_one();
}
int consume() {
    std::unique_lock<std::mutex> lk(m);
    cv_not_empty.wait(lk, []{ return !q.empty(); });
    int v = q.front(); q.pop();
    lk.unlock();
    cv_not_empty.notify_one();      // galat bell? note dekho
    return v;
}

Solution ke andar correction: jab ek consumer ek item remove karta hai toh use cv_not_full.notify_one() bajani chahiye (ek slot khula), aur jab producer ek item add karta hai toh cv_not_empty.notify_one() bajata hai. Har side doosri side ko jagaati hai. Dono ke liye ek CV use karne par har baar notify_all() force hoga aur wakeups waste honge — do CVs har notify ko precise banaate hain. State diagram dekho:

Figure — std - condition_variable

L4.2 — Capacity edge cases

Upar ke bounded buffer ke liye, do extremes par kya hota hai: (a) N = 0, aur (b) N total produce hone wale items se bada?

Recall Solution

(a) N = 0: producer predicate q.size() < 0 kabhi bhi true nahi hota (size unsigned hai, hamesha ), isliye har producer hamesha ke liye block ho jaata hai — ek deadlock. Capacity-0 buffer degenerate hai; ek real "rendezvous" (size-0 handoff) ke liye alag design chahiye jahan producer aur consumer directly milte hain. (b) N bahut bada: cv_not_full predicate hamesha true hai, isliye producers actually kabhi wait nahi karte — bounded buffer gracefully parent note ke unbounded producer-consumer queue mein degrade ho jaata hai. Correct hai, bas koi back-pressure nahi.


Level 5 — Mastery (subtle correctness & performance)

L5.1 — Predicate jo do variables padhta hai

Ek consumer cv.wait(lk, []{ return !q.empty() || done; }) ke saath wait karta hai. q aur done dono predicate ke andar read hote hain. Kya yeh safe hai? Kya guarantee karta hai ki predicate dono ka ek consistent, up-to-date view dekhta hai?

Recall Solution

Safe hai — kyunki predicate hamesha mutex held hone par evaluate hota hai. wait(lk, pred) pred() sirf tab call karta hai jab lk m own karta hai. Har writer (q.push, done = true) bhi m hold karta hai. Isliye q aur done ke saare reads aur writes usi mutex se serialize hote hain: predicate ek consistent snapshot dekhta hai. Us shared mutex ke bina tumhare paas data race hota aur tumhe uski jagah std::atomic aur careful memory ordering chahiye hota.

L5.2 — Timed wait return value

cv.wait_for(lk, 100ms, pred) ek bool return karta hai. Return hone ke baad, tum kaise distinguish karte ho "predicate true ho gaya" aur "timed out"? Returned bool kya hai?

Recall Solution

wait_for(lk, dur, pred) pred() ki value return karta hai jaise wait khatam hone ke baad evaluate ki gayi. Toh:

  • true return kare ⇒ predicate satisfied hai (aage badho).
  • false return kare ⇒ timeout elapsed ho gaya aur predicate abhi bhi false hai (timeout handle karo).

Predicate overload use karte waqt tumhe alag cv_status check ki zaroorat nahi — returned bool already batata hai tum kaun se case mein ho.

L5.3 — Busy-wait vs CV wait ki cost

Maano ek core cycles/second run karta hai. Ek busy-wait while(q.empty()){} item aane se pehle 5 seconds spin karta hai; har spin iteration 4 cycles ki hai. (a) Roughly kitni wasted spin iterations hain? (b) CV wait iske bajaye sleep karta hai aur ~0 CPU use karta hai. Qualitative win batao.

Recall Solution

(a) 5 s mein available cycles cycles. 4 cycles/iteration par: Yeh ~2.5 billion pointless loop turns hain jo ek full core jala rahe hain. (b) Condition_variable wait thread ko so deta hai (OS use de-schedule karta hai), isliye woh 5 seconds mein ~0 CPU lagti hai aur core real kaam ke liye free ho jaata hai — including, crucially, woh producer jo item create karne ke liye run karna chahiye. Yahi reason hai ki condition_variable exist karta hai.


Recall Feynman check: poora ladder ek saanst mein bolo

Ek condition_variable bina memory ka ek doorbell hai. Tum ise hamesha ek shared flag aur ek mutex ke saath pair karte ho; waiter unique_lock ke zariye unlock-and-sleep karta hai, flag ko while loop mein dobara check karta hai (spurious/stolen wakeups), aur notifier flag ko lock ke under change karta hai phir bajata hai. Two-sided flow (bounded buffer) ko do bells chahiye; edge cases (N=0, bada N) deadlock aur unbounded hain; aur har predicate variable mutex ke under rehta hai ya atomic hai.