Visual walkthrough — Concurrency — std - thread, std - mutex, std - lock_guard, std - unique_lock
5.2.24 · D2· Coding › C++ Programming › Concurrency — std - thread, std - mutex, std - lock_guard, s
Hum ek central result build karte hain: kyun do threads jo ek shared counter mein 100000 add kar rahe hain, 2 se lekar 200000 tak koi bhi value pe khatam ho sakte hain — aur kyun add ko lock mein wrap karna hamesha exactly 200000 deta hai.
Step 1 — "Thread" kya hota hai, do haathon ke roop mein
KYA. Ek thread execution ki ek single line hai — socho ek haath instructions ki list pe neeche ja raha hai, unhe ek ke baad ek karta hua. Do threads = do haath ek saath chal rahe hain apni apni instruction lists pe.
YE YAHAN SE KYUN SHURU. Race dekhne se pehle, humein dekhna hoga ki "ek saath" literally hai: do haath shuddh shaalinta se baari nahi le rahe. Kuch bhi unhe coordinate nahi karta jab tak hum coordination add nahi karte.
PICTURE. Do haath, dono ek hi chhota program ++counter chala rahe hain, dono ek shared box ki taraf badh rahe hain jis par counter likha hai.
Parent ka kitchen analogy ek line mein: do cooks, ek counter. Poora model dekhne ke liye parent topic dekhein.
Step 2 — ++counter actually kya hai: teen chhote moves
KYA. Ek akela symbol ++counter hardware ke liye ek action nahi hai. Yeh teen hain:
Symbols ko left se right padhte hain:
- — ek private scratch value jo thread ke apne CPU register mein rehti hai; har haath ka apna alag hota hai.
- — READ: box ka current number apne private mein copy karo.
- — ADD: sirf apni copy mein ek add karo. Box yahan untouched rehta hai.
- — WRITE: apna private wapas shared box mein stamp karo.
YEH DECOMPOSITION KYUN. Poori bimari READ aur WRITE ke beech ke gap mein rehti hai. Agar hum ++counter ko ek instant action maante, koi race exist nahi kar sakti. Ise teen mein todna interruptible middle ko expose karta hai.
PICTURE. Teen arrows READ → ADD → WRITE, private ek chhote side-pad ke roop mein shared box se alag draw kiya hua.
Step 3 — Ek possible interleaving jo count kho deta hai
KYA. Ab dono haathon ko apne teen moves chalane do, aur time ko ek shared timeline pe tick karne do. Maano box se shuru hota hai. Yahan ek buri interleaving hai (bahut saari mein se jo OS choose kar sakta hai):
| time | Thread A | Thread B | box counter |
|---|---|---|---|
| READ | |||
| READ | |||
| ADD | |||
| ADD | |||
| WRITE | |||
| WRITE |
- — do haathon ki alag alag scratch values.
- Dono ne read kiya pehle ki kisi ne bhi likha → dono ne compute kiya → box pe khatam, nahi.
GALAT KYUN HAI. Do ++ operations box ko se badhani chahiye thi (yani tak). Yahan ek increment gayab ho gaya kyunki B ne stale value read ki jo A ne abhi update nahi ki thi.
PICTURE. Timeline jismein dono READs same value pe land kar rahe hain (red), dono WRITEs same stamp kar rahe hain (orange) — woh collision jo count khaata hai.
Step 4 — Yeh kitna bura ho sakta hai? Outcomes ki poori range
KYA. 100000 increments per thread ke saath, agar hum Step 2 ke read/write model ko sach maanein, sampled value kuch bhi ho sakti hai is interval mein
bounds padhte hain:
- Upper — lucky run jahan koi bhi do operations kabhi overlap nahi karte; increments mein se har ek bachta hai.
- Lower — worst interleaving. Maano thread A pehle apne saare READs karta hai, har baar value apne private mein grab karta hai (box kabhi aage nahi badhta kyunki A kabhi write nahi karta abhi tak). Phir thread B completion tak chalta hai, box ko tak drive karta hai. Aakhir mein A apne writes karta hai, har baar ek stale wapas stamp karta hai — aakhri write box ko sirf pe chhod deta hai. Dono threads mein symmetric bookkeeping se true theoretical floor ban jaata hai (har thread ki final write wahi hai jo guaranteed land hoti hai). Yeh nahi hai: koi bhi cheez ek poore thread ke kaam ko preserve hone par force nahi karti.
ACTUAL FLOOR KYUN MAAYENE RAKHTA HAI. Ek tempting galat guess yeh hai ki "kam se kam ek thread ke 100000 survive karte hain, toh ." Yeh galat hai — stale reads almost sab kuch wipe out kar sakti hain. Floor ko jaana jaanna yeh batata hai ki ek race ek minor rounding error nahi hai; yeh almost saara kaam tabah kar sakta hai.
YEH SIRF EK CARICATURE KYUN HAI. Kyunki access UB hai, "kahi mein" bhi ek promise nahi hai — ek real run crash ho sakta hai ya kisi bhi counting argument se bahar garbage print kar sakta hai. Range ek teaching lower/upper bound on the counting model hai, koi spec nahi.
PICTURE. se tak number line jismein possible results scattered hain — baat yeh hai ki koi single "answer" nahi hai.
Step 5 — Fix: READ→ADD→WRITE ko unsplittable banao
KYA. Teen moves ke around ek mutex (ek single token) rakho. Ek haath ko andar jaane ke liye token hold karna zaroori hai; sirf ek token exist karta hai.
std::mutex mtx;
{ std::lock_guard<std::mutex> lk(mtx); // grab token here
++counter; // READ-ADD-WRITE, uninterrupted
} // lk destroyed -> drop token heremtx— single token object.lk— RAII grip. RAII ka matlab hai Resource Acquisition Is Initialization: yeh idea ki ek resource ka ownership (yahan, lock) ek object ke lifetime se tied hai — yeh constructor mein acquire karta hai aur destructor mein release karta hai. Dekhein RAII and resource management. Tohlktoken ko build hone par grab karta hai aur block khatam hone par drop karta hai — chahe exception bhi fly kare.- Block
{ ... }— critical section: ek baar mein zyada se zyada ek haath andar.
KAAM KYUN KARTA HAI. Doosra haath ab READ aur WRITE ke beech nahi slip kar sakta — woh token pe blocked hai jab tak pehla haath teeno moves finish karke token drop nahi karta. Step 3 mein jo gap counts kho raha tha woh ab exist nahi karta.
PICTURE. Thread A token hold karte hue, READ→ADD→WRITE bina kisi interruption ke karta hua; Thread B band gate par frozen, wait karta hua.
Step 6 — Token ke saath same start dekhna: koi count nahi khoota
KYA. Step 3 ka start () replay karo lekin token se serialized:
| time | Thread A | Thread B | box counter |
|---|---|---|---|
| lock, READ | blocked | ||
| ADD | blocked | ||
| WRITE , unlock | blocked | ||
| lock, READ | |||
| ADD | |||
| WRITE , unlock |
B ka READ ab dekhta hai — woh fresh value jo A ne likhi — toh B compute karta hai. Do increments, box badhta hai. Sahi. Aur kyunki lock proper synchronization bhi establish karta hai, yeh ab defined behavior hai — ek real promise, koi caricature nahi.
YEH POORA POINT KYUN HAI. Serialization "ek saath" ko sirf shared box ke liye "ek-ek karke" mein wapas badal deta hai. Dono haath baki jagah parallel chalte rehte hain; woh sirf token par queue karte hain.
PICTURE. Corrected timeline: B ka READ arrow ab pe land karta hai, stale pe nahi.
Step 7 — Degenerate cases (taaki aap kisi unseen mein na faso)
KYA & KYUN, har ek ek aisa corner jismein reader stumble kar sakta hai:
PICTURE. 2×2 grid: (single hand → safe), (many readers → safe), (writer + reader unlocked → race), (writer + reader locked → safe).
Ek-picture summary
Upar sab kuch compress kiya: unprotected path READ/ADD/WRITE ko split karta hai aur ek count kho deta hai; protected path trio ko poora run karne par majboor karta hai, toh count bachta hai.
Recall Feynman Retelling — poora walkthrough simple shabdon mein
Do bachche ek shared card par likhe ek number ko ek ek karke badhana chahte hain. Badhane ke liye, ek bachche ko (1) number read karna hota hai, (2) apne dimaag mein ek add karna hota hai, (3) purana number mitaana aur naya likhna hota hai. Problem yeh hai: jab bachcha A abhi bhi dimaag mein add kar raha hai, bachcha B card se purana number read kar leta hai. Dono same naya number likhte hain — toh do bumps ne card ko sirf ek badhaya. Ek poora bump gayab ho gaya, aur kyunki yeh split-second timing par depend karta hai, aap ise reliably reproduce nahi kar sakte — actually, kyunki yeh "undefined behavior" hai, card almost kuch bhi dikhaa sakta hai, chahe kuch bhi silly ho. Sabse buri timing mein, almost har bump wipe ho jaata hai. Fix hai ek token table par: card tab hi touch kar sakte ho jab token haath mein ho, aur exactly ek token hai. Ab bachcha B tab tak read nahi kar sakta jab tak bachcha A likhna finish karke token nahi chhod deta — B ko hamesha A ki fresh value milti hai. lock_guard ek jaadui strap hai: jis pal bachcha table se door jaata hai, token automatically gir jaata hai, toh koi bhi use haath mein pakde nahi ja sakta (yeh aadat ki "jaana" ko "chodna" se baandhna wahi hai jo bade log RAII kehte hain). Same parallel bachche, same shared card — lekin touching ab single-file hai, aur har bump count karta hai.
Connections
- RAII and resource management —
lock_guardgrip jo token auto-drop karti hai - std::atomic and lock-free programming —
++counterko bina lock ke ek unsplittable step banao - std::condition_variable — jab ek haath ko sirf token ke liye nahi, ek condition ke liye wait karna ho
- Deadlock and lock ordering — do tokens ke saath kya galat hota hai
- Undefined behavior in C++ — data race formally kya hoti hai
- Move semantics — kyun ek
std::thread(ek unique haath ka owner) move-only hota hai