ยง28.12โ€“28.14Too Much Spinning: What Now? โ€ฆ Using Queues: Sleeping Instead Of Spinning

Part II OSTEP pp. 327โ€“331 ยท ~5 min read

  • park
  • priority inversion
  • priority inheritance

Hardware bought correctness (TAS) and even fairness (tickets) โ€” but every design so far spins, and spinning has a worst case the OS must rescue us from.

28.12 Too Much Spinning: What Now?

Two threads, one CPU: the holder is preempted mid-critical-section; the other thread spins its entire time slice checking a flag that cannot change โ€” the holder isnโ€™t running. With N threads, Nโˆ’1 time slices can burn this way per incident.

The Crux: How To Avoid Spinning

How can we develop a lock that doesnโ€™t needlessly waste time spinning on the CPU?

Hardware support alone cannot solve this. Weโ€™ll need the OS.

28.13 A Simple Approach: Just Yield, Baby

First idea: when you would spin, yield instead (Figure 28.8):

void lock() {
    while (TestAndSet(&flag, 1) == 1)
        yield(); // give up the CPU
}

yield() deschedules the caller โ€” running โ†’ ready โ€” promoting someone else. For two threads on one CPU it works nicely: the waiter steps aside, the holder finishes. But scale to 100 contending threads: one holder preempted, and the other 99 each run, find the lock held, and yield โ€” 99 context switches per incident, cheaper than 99 slices but far from free. Worse: starvation is untouched โ€” a thread can be caught in an endless run-and-yield loop while others slip through the lock repeatedly. Too much is still left to chance.

28.14 Using Queues: Sleeping Instead Of Spinning

The real problem: the scheduler keeps choosing who tries next. To control succession explicitly we need a queue of waiters, and OS primitives to sleep and wake precisely: park() (sleep the caller) and unpark(threadID) (wake that one), per Solaris. The lock (Figure 28.9):

typedef struct __lock_t {
    int flag;
    int guard;
    queue_t *q;
} lock_t;

void lock(lock_t *m) {
    while (TestAndSet(&m->guard, 1) == 1)
        ; // acquire guard lock by spinning
    if (m->flag == 0) {
        m->flag = 1; // lock is acquired
        m->guard = 0;
    } else {
        queue_add(m->q, gettid());
        m->guard = 0;
        park();
    }
}

void unlock(lock_t *m) {
    while (TestAndSet(&m->guard, 1) == 1)
        ; // acquire guard lock by spinning
    if (queue_empty(m->q))
        m->flag = 0; // let go of lock; no one wants it
    else
        unpark(queue_remove(m->q)); // hold lock
                                    // (for next thread!)
    m->guard = 0;
}

Step through its three defining moments โ€” including the race that almost breaks it:

The queue lock in motion: sleep instead of spin, and a direct handoff at wake
Thread 1(holder)
Thread 2(waiter)
OS
Acquire, contend, sleep
Release: hand off, don't re-fight
The wakeup/waiting race โ€” and setpark()
step 1 / 11 ยท time flows downward

The guard is a tiny TAS spin lock protecting the lock's OWN bookkeeping โ€” held for a few instructions, never for the user's critical section.

Three subtleties, restated. The guard means spinning isnโ€™t gone, just bounded โ€” a few bookkeeping instructions, not a user-defined critical section. The direct handoff (flag never returns to 0 between holder and successor) is a necessity: the woken thread canโ€™t safely re-acquire anything at its wake point. And the wakeup/waiting race โ€” unparked before you park, then sleeping forever โ€” is closed by setpark():

queue_add(m->q, gettid());
setpark(); // new code
m->guard = 0;

(An alternative passes the guard into the kernel, which can then atomically release it and dequeue the sleeper.)

Aside: Priority Inversion โ€” More Reason To Avoid Spinning

Beyond waste, spin locks can break CORRECTNESS on priority schedulers. T1 (low priority) holds a spin lock; T2 (high) wakes, preempts T1, and spins for the lock โ€” forever, since T1 can never run under a scheduler that always prefers T2. The system hangs. Nor does avoiding spin locks fully save you: with T3 > T2 > T1 and T1 holding a lock T3 wants, middle-priority T2 can run indefinitely while mighty T3 waits on lowly T1 โ€” priority inversion , an intergalactic scourge (it struck the Mars Pathfinder). Remedies: priority inheritance (boost the holder to its waiterโ€™s priority), avoiding spin locks, or the bluntest: give everything the same priority.

Check yourself

1.Quantify the spinning disaster: N threads, one CPU, and the lock holder gets preempted mid-critical-section.

2.The 'just yield, baby' lock replaces spinning with yield(). What does yield do, and where does this design still bleed?

3.In the queue lock (Figure 28.9), what is the guard variable โ€” and doesn't it reintroduce spinning?

4.On wakeup, the flag is NOT set back to 0 and re-fought-over. Why is handing the lock directly to the unparked thread a necessity, not a nicety?

5.The wakeup/waiting race, and Solaris's fix:

5 questions