ยง28.6โ€“28.8A Failed Attempt: Just Using Loads/Stores โ€ฆ Evaluating Spin Locks

Part II OSTEP pp. 319โ€“322 ยท ~5 min read

  • spin lock
  • test-and-set

Interrupt control is out. Time to build from ordinary memory โ€” and discover why ordinary memory isnโ€™t enough.

28.6 A Failed Attempt: Just Using Loads/Stores

The obvious design (Figure 28.1): a flag, tested then set:

typedef struct __lock_t { int flag; } lock_t;

void init(lock_t *mutex) {
    // 0 -> lock is available, 1 -> held
    mutex->flag = 0;
}

void lock(lock_t *mutex) {
    while (mutex->flag == 1) // TEST the flag
        ; // spin-wait (do nothing)
    mutex->flag = 1;          // now SET it!
}

void unlock(lock_t *mutex) {
    mutex->flag = 0;
}

Two problems, one of correctness and one of performance. The correctness failure takes exactly one untimely interrupt:

Figure 28.2: the flag lock, dismantled by one well-placed interrupt (flag starts at 0)
Thread 1
Thread 2
Both threads enter โ€” mutual exclusion, failed
step 1 / 7 ยท time flows downward

Thread 1: call lock()

The performance problem is the waiting style itself: spin-waiting โ€” endlessly checking the flag, burning cycles; exceptionally wasteful on a uniprocessor, where the thread being waited for canโ€™t even run.

Tip: Think About Concurrency As A Malicious Scheduler

Pretend youโ€™re a scheduler that interrupts threads at the most inopportune moments, purely to foil their feeble synchronization attempts. An improbable interleaving is still a POSSIBLE one โ€” and possible is all it takes to prove a design broken. It can be useful to think maliciously (at least sometimes).

28.7 Building Working Spin Locks With Test-And-Set

Since plain loads and stores canโ€™t fuse the test with the set, hardware does it for us โ€” support dating to the earliest multiprocessors (Burroughs B5000, early 1960s), present everywhere today. The test-and-set (atomic exchange) instruction, described as C:

int TestAndSet(int *old_ptr, int new) {
    int old = *old_ptr; // fetch old value at old_ptr
    *old_ptr = new;     // store 'new' into old_ptr
    return old;         // return the old value
}

โ€” all of it atomic. Return the old value, install the new one, no seam in between. Which makes the lock almost embarrassingly short (Figure 28.3):

void lock(lock_t *lock) {
    while (TestAndSet(&lock->flag, 1) == 1)
        ; // spin-wait (do nothing)
}

void unlock(lock_t *lock) {
    lock->flag = 0;
}

Two cases prove it. Lock free (flag 0): TestAndSet returns 0 โ€” the caller exits the loop having already set the flag to 1 in the same atomic act; acquired. Lock held (flag 1): TestAndSet returns 1 (and re-stores 1, harmlessly) โ€” the caller spins, and spins, until some unlock stores 0; the next TestAndSet returns 0-and-sets-1, and the lock changes hands. One winner per release, guaranteed by atomicity.

This is the spin lock : the simplest lock there is, spinning CPU cycles until the lock frees. One requirement on a single CPU: a preemptive scheduler โ€” without the timer, a spinner never relinquishes the processor and the holder never runs.

Aside: Dekkerโ€™s And Petersonโ€™s Algorithms

In the 1960s Dijkstra posed the mutual-exclusion problem; the mathematician Theodorus Jozef Dekker solved it with ONLY loads and stores โ€” later refined by Peterson: a flag per thread (โ€œI intend to enterโ€) plus a turn variable (โ€œbut after youโ€), spinning only while the other intends AND itโ€™s the otherโ€™s turn. Elegant โ€” and twice retired: assuming a little hardware support proved far easier (and such support already existed), and relaxed memory-consistency models on modern hardware break these algorithms outright. Yet more research relegated to the dustbin of historyโ€ฆ

28.8 Evaluating Spin Locks

The TAS spin lock, graded on the rubric
verdict
mutual exclusionโœ“ correct
fairnessโœ— none
performance: one CPUโœ— painful
performance: many CPUsโ‰ˆ reasonable
Dotted-underlined cells have explanations โ€” click one.

A correct lock, at last โ€” with a fairness hole and a scheduler-shaped weak spot. Both get fixed; first, a tour of the other atomic instructions hardware offers.

Check yourself

1.The flag lock (test the flag, then set it) fails. Where exactly is the fatal seam?

2.What does the test-and-set instruction do, and why does that single property rescue the flag design?

3.Grade the TAS spin lock on the chapter's three axes.

4.Why does a spin lock on a SINGLE processor require a preemptive scheduler at all?

5.Dekker's and Peterson's algorithms achieve mutual exclusion with ONLY loads and stores. Why does the book file them under 'dustbin of history'?

5 questions