Synchronization primitives (locks, barriers)
6.1.8· Hardware › Parallelism & Multicore
The fundamental problem: Race conditions
Jab multiple threads concurrently execute hoti hain, unke operations interleave ho sakte hain unpredictable tareekon se. Yeh simple increment dekho:
// Thread 1 aur Thread 2 dono execute karte hain:
counter = counter + 1;Hardware level par, yeh ban jaata hai:
- LOAD counter memory se register mein
- ADD 1 register mein
- STORE register wapas memory mein
Agar dono threads simultaneously execute hon, toh hum dekh sakte hain:
- Thread 1 LOAD (reads 0)
- Thread 2 LOAD (reads 0)
- Thread 1 ADD (computes 1)
- Thread 2 ADD (computes 1)
- Thread 1 STORE (writes 1)
- Thread 2 STORE (writes 1)
Result: counter = 1, 2 nahi! Yeh hai lost update problem.

Locks (Mutual Exclusion)
Derivation: Lock ko first principles se banana
Ek sahi lock ki requirements:
- Mutual exclusion: Critical section mein sirf ek thread
- Progress: Agar koi thread lock nahi rakh rahi, toh ek waiting thread eventually usse acquire kar le
- Bounded waiting: Lock acquire karne ka wait karne wali thread ko forever wait nahi karna chahiye (fairness)
Attempt 1: Simple flag (BROKEN)
bool lock = false;
acquire() {
while (lock == true) { /* spin */ }
lock = true;
}
release() {
lock = false;
}Yeh kyun fail hota hai: Do threads dono lock == false dekh sakti hain, dono while loop pass kar sakti hain, aur dono lock = true set kar sakti hain. Koi mutual exclusion nahi!
Attempt 2: Test-and-Set (Hardware atomic instruction)
Modern CPUs atomic read-modify-write instructions provide karte hain. Test-and-Set (TAS):
// Hardware yeh EK atomic instruction ke roop mein provide karta hai
int TestAndSet(int *ptr) {
int old = *ptr;
*ptr = 1;
return old;
}Atomic kyun? CPU ka cache coherence protocol ensure karta hai ki TAS operation ke dauran koi doosra core is memory location ko access nahi kar sakta.
Ab hamaara lock:
typedef struct {
int flag; // 0 = free, 1 = held
} lock_t;
void acquire(lock_t *lock) {
while (TestAndSet(&lock->flag) == 1) {
// Spin: koshish karte raho
}
}
void release(lock_t *lock) {
lock->flag = 0;
}Correctness ki derivation:
- Mutual exclusion: TAS exactly EK thread ko 0 (old value) return karta hai jab flag 0 tha. Baaki sab ko 1 milta hai aur woh spin karte hain.
- Progress: Jab release() flag=0 set karta hai, next TAS succeed karega.
- Bounded waiting: Guarantee nahi—ek thread forever spin kar sakti hai agar unlucky ho.
Attempt 3: Compare-and-Swap (CAS) - Zyada general
int CompareAndSwap(int *ptr, int expected, int new) {
int actual = *ptr;
if (actual == expected)
*ptr = new;
return actual;
}CAS zyada powerful hai—lock-free data structures implement kar sakta hai. Lock implementation behavior mein TAS jaisi hi hai.
Blocking locks: CPU waste mat karo
Spin locks single-core ya oversubscribed systems par wasteful hote hain. Solution: Lock unavailable hone par CPU yield karo.
void acquire(lock_t *lock) {
while (TestAndSet(&lock->flag) == 1) {
yield(); // OS call: CPU doosri thread ko do
}
}Aur bhi achha: Ek wait queue use karo. Jab lock unavailable ho, thread park() call kare (so jaye), aur release karne wali thread ek waiting thread par unpark() call kare.
Barriers (Synchronization points)
Derivation: Barrier banana
Requirements:
- Saari threads ko barrier tak pahunchna chahiye
- Koi thread tab tak proceed nahi karti jab tak sab pahunch na jayein
- Multiple phases ke liye reusable (back-to-back barriers mein safely kaam karna chahiye)
Naive barrier (BROKEN reuse ke liye)
// DANGER: count reset karna aur ek CV use karna kaafi nahi hai
void barrier_wait(barrier_t *b) {
lock(&b->lock);
b->count++;
if (b->count == b->threshold) {
b->count = 0; // reset
broadcast(&b->cv);
} else {
wait(&b->cv, &b->lock); // doosron ka wait karo
}
unlock(&b->lock);
}Correct reusable barrier (generation counter ke saath)
typedef struct {
int count; // IS generation mein kitne pahunche hain
int threshold; // Threads ki total sankhya (N)
int generation; // Phase / episode counter
lock_t lock;
cond_t cv;
} barrier_t;
void barrier_init(barrier_t *b, int n) {
b->count = 0;
b->threshold = n;
b->generation = 0;
lock_init(&b->lock); // internal lock ZAROOR initialize karo
cond_init(&b->cv); // condition variable ZAROOR initialize karo
}
void barrier_wait(barrier_t *b) {
lock(&b->lock);
int my_gen = b->generation; // yaad rakho main kis episode ka hoon
b->count++;
if (b->count == b->threshold) {
// Last thread: barrier kholo aur NEW generation shuru karo
b->generation++; // phase advance karo -> purane aur naye ko alag karo
b->count = 0; // next use ke liye reset karo
broadcast(&b->cv);
} else {
// Tab tak wait karo jab tak generation actually change na ho.
// Looping spurious wakeups AUR reuse race dono se bachata hai.
while (my_gen == b->generation) {
wait(&b->cv, &b->lock);
}
}
unlock(&b->lock);
}Yeh kyun kaam karta hai:
- Lock count/generation protect karta hai: Atomic increment aur comparison.
- Generation counter: Ek naya-woken fast thread jo next phase ke liye re-enter karti hai uska
my_genalag hota hai, isliye woh pichle episode ke wait ko accidentally satisfy ya interfere nahi kar sakti. whileloop (ifnahi): Thread tab tak nahi nikalti jab takgenerationsach mein advance na ho, isliye spurious wakeups aur reuse races dono handle hote hain.- Init function:
lock_initaurcond_initrequired hain—uninitialized mutex/CV undefined behavior hai.
Barrier ke types
- Blocking barrier: Threads so jaati hain (upar wala example)
- Spinning barrier: Threads spin-wait karti hain (bahut chote phases ke liye useful)
- Tree barrier: ke liye hierarchical wake-up (notification time se ho jaata hai)
Hardware support for synchronization
Modern CPUs provide karte hain:
- Atomic instructions: TAS, CAS, Fetch-and-Add, Load-Linked/Store-Conditional
- Memory barriers: Cores ke across loads/stores ki ordering enforce karte hain
- Cache coherence: Lock variables consistent rakhta hai (MESI protocol)
Advanced: Lock-free algorithms
Kya hum sirf atomic CAS use karke locks se poori tarah bach sakte hain?
void lock_free_increment(int *counter) {
int old, new;
do {
old = *counter;
new = old + 1;
} while (CompareAndSwap(counter, old, new) != old);
}Kaise kaam karta hai:
- Current value read karo
- Naya value compute karo
- CAS: agar
*counterabhi bhioldke equal hai,newmein update karo - Agar CAS fail ho (doosri thread ne badla), retry karo
Advantage: No blocking, hamesha progress
Disadvantage: High contention → bahut saare retries → wasted work
Recall Ek 12-saal ke bachche ko explain karo
Socho ek bathroom mein ek toilet hai. Agar lock nahi hai, toh do log ek saath andar aa sakte hain—awkward! Ek lock ek simple system ki tarah hai: "Agar darwaza unlocked hai, main usse lock karta hoon aur andar jaata hoon. Jab kaam ho jaaye, unlock kar deta hoon." Test-and-Set ek magic door handle ki tarah hai jo ek instant mein, check bhi karta hai ki unlock hai ya nahi AUR lock bhi kar deta hai agar hai. Koi in dono actions ke beech ghus nahi sakta.
Ek barrier ek field trip ki tarah hai jahan teacher kehti hai, "Hum sab 3 baje bus par milenge—jab tak sab nahi aa jaate, koi nahi chadhega." Agar tum jaldi aa jaao, wait karo. Jab last banda aa jaye, teacher chillati hai "All aboard!" aur sab saath chadhte hain. Aur agli baar phir trip chalane ke liye, teacher ek "Day number" rakhti hai taaki ek fast bachcha jo utar chuka hai galti se aaj ki roll call mein count na ho.
Connections
- Cache coherence protocols – MESI ensure karta hai ki lock variables cores ke across consistent rahein
- Memory consistency models – Loads/stores ki legal orderings define karte hain; synchronization order enforce karta hai
- Thread scheduling – Blocking locks ko OS scheduler ki zaroorat hoti hai threads ko park/unpark karne ke liye
- Deadlock and livelock – Galat lock ordering deadlock cause karta hai; lock-free algorithms livelock kar sakte hain
- Parallel algorithms – Barriers bulk-synchronous parallel (BSP) model enable karte hain
- Atomic operations – Hardware primitives jo lock implementation possible banate hain
#flashcards/hardware