Mutual Exclusion

Spinlock

A spinlock implements mutual exclusion by busy-waiting on an atomic lock variable. The waiter loops, repeatedly checking the lock until it becomes available, instead of blocking on a queue. Spinlocks are the simplest possible lock: no blocked list, no scheduler cooperation, just one atomic word.

When is spinning the right choice?

When the expected wait is shorter than two context switches. If the critical section is tiny and uncontended, the cost of blocking (save registers, queue, reschedule, reload) dominates the cost of spinning a few cycles. On a uniprocessor the spinning task only wastes its own time-slice; on a multiprocessor, other cores can actually release the lock while you spin.

Basic form

Built directly on an atomic hardware instruction, typically Test-and-Set Instruction.

Yielding spinlocks

Pure spinning wastes CPU on a uniprocessor because no other core can release the lock while you hold the time-slice. A task can relinquish its slice after each failed check:

while ( TestSet( Lock ) == CLOSED ) uThisTask().yield();

On a multiprocessor, you usually yield after N failed checks instead of after one. A spinlock that tunes this N at runtime is an adaptive spinlock.

Failure property

Spinlock implementations generally break rule 5 of mutual exclusion (bounded waiting). There is no guarantee of eventual progress: a stream of contending tasks can starve one waiter indefinitely. In practice, starvation is rare under typical workloads.

uC++ variants

uC++ ships two spinning locks, both built on a hardware atomic:

class uSpinLock {                 class uLock {
public:                           public:
    uSpinLock();                    uLock( unsigned int value = 1 );
    void acquire();                 void acquire();
    bool tryacquire();              bool tryacquire();
    void release();                 void release();
};                                };
  • uSpinLock is non-yielding. It never gives up its time-slice.
  • uLock is yielding. It yields on failed acquire, suitable when contention is expected.
  • tryacquire() attempts once and returns; does not wait.
  • Starts closed (0) or open (1); waiters race to acquire after a release.
  • Not meaningful to copy, assign, or pass a lock by value.

Synchronization with uLock

One task waits on a closed lock; the other releases once its prerequisite completes.

_Task T1 {                      _Task T2 {
    uLock & lk;                   uLock & lk;
    void main() {                 void main() {
        S1;                         lk.acquire();
        lk.release();               S2;
    }                             }
};
 
int main() {
    uLock lock( 0 );   // closed; T2 will block until T1 releases
    T1 t1( lock );  T2 t2( lock );
}

Mutual exclusion with uLock

Initialize open; tasks acquire before the critical section, release after.

uLock lock( 1 );                // open
lk.acquire();
// critical section
lk.release();