Sleeping Instead of Spinning: Queue-Based Locks
Sleeping Instead of Spinning: Queue-Based Locks
1. Why Spinning and Yielding Are Not Enough
Earlier locking approaches had serious drawbacks:
Spin locks
-
Waste CPU cycles by busy waiting
-
Terrible on single-CPU systems
-
Can cause priority inversion
-
No fairness → starvation possible
Yield-based locks
-
Reduce spinning
-
Still cause many context switches
-
Still no control over which thread gets the lock next
-
Starvation still possible
Core Problem
The scheduler decides who runs next — not the lock.
If the scheduler makes poor decisions, threads either spin or yield repeatedly, wasting time and still not guaranteeing fairness.
2. Key Idea: Explicit Control with Queues
The solution is to explicitly control who gets the lock next.
👉 Instead of:
-
Spinning (wasting CPU), or
-
Yielding randomly (scheduler decides),
we:
-
Put waiting threads to sleep
-
Maintain a queue of waiting threads
-
Wake up exactly one thread when the lock is released
This requires OS support.
3. Required OS Support
The solution uses two OS primitives (as in Solaris):
| Primitive | Purpose |
|---|---|
park() | Put the calling thread to sleep |
unpark(threadID) | Wake up a specific thread |
These allow threads to block instead of spinning.
4. Data Structures Used
Meaning of Fields
-
flag
-
0→ lock is free -
1→ lock is held
-
-
guard
-
A small spin lock protecting
flagand the queue -
Held only for a few instructions
-
-
queue
-
Stores thread IDs of waiting threads
-
Ensures FIFO ordering → fairness
-
5. How Lock Acquisition Works
Step-by-Step (lock())
Acquire the guard spin lock (very short critical section)
✅ No spinning
✅ No yielding loops
✅ No CPU waste
Diagram: Lock Acquisition
6. How Lock Release Works
Step-by-Step (unlock())
Acquire the guard lock
-
Release the guard
Diagram: Lock Release
7. Why flag Is NOT Set to 0 in This Case
This is subtle but crucial.
-
The awakened thread resumes after
park() -
It does not re-enter
lock() -
So it never sets
flag = 1
👉 Therefore, the lock is passed directly
from the unlocking thread to the next thread.
This avoids races and extra locking overhead.
8. Why a Guard Spinlock Is Still Used
You might notice:
“Wait — aren’t we still spinning?”
Yes, but only briefly.
-
Guard protects very small critical sections
-
Only a few instructions
-
No user code inside
✅ This is acceptable
❌ Spinning on long critical sections is not
9. Avoiding Starvation
This approach explicitly avoids starvation:
-
Threads are queued
-
FIFO order is enforced
-
Each waiting thread will eventually run
✔ Mutual exclusion
✔ Progress
✔ Bounded waiting
10. Wakeup/Waiting Race (Important Detail)
The Problem
A thread might:
-
Add itself to the queue
-
Get interrupted
-
Another thread releases the lock and calls
unpark -
Then the first thread calls
park()→ sleeps forever
Solution: setpark()
-
Signals intent to sleep
-
Ensures
park()won’t miss a wakeup
11. Priority Inversion (Why This Helps)
Spin locks can cause priority inversion:
-
High-priority thread spins
-
Low-priority thread holds lock but never runs
Sleeping locks:
-
Block waiting threads
-
Allow scheduler to run lock holder
-
Can work with priority inheritance
12. Big Picture Summary
| Approach | CPU Waste | Fairness | Starvation |
|---|---|---|---|
| Spin lock | High | No | Yes |
| Yield lock | Medium | No | Yes |
| Queue + sleep | Low | Yes | No |
13. Key Takeaways
-
Queue-based locks sleep instead of spin
-
Require OS primitives (
park,unpark) -
Maintain explicit wait queues
-
Ensure fairness and prevent starvation
-
Used in real systems (Solaris, Linux futexes)
-
Spinning is limited to very short guard sections

Comments
Post a Comment