C++
C++11
Spinlock
Atomic Operations
Multithreading

C11 Implementation of Spinlock using header atomic

Master System Design with Codemia

Enhance your system design skills with over 120 practice problems, detailed solutions, and hands-on exercises.

Introduction

A spinlock is a minimal lock that repeatedly checks a flag until the lock becomes available. In C11, the stdatomic.h header provides the atomic primitives needed to build one safely. This article shows a correct implementation, explains memory ordering, and highlights when spinlocks should not be used.

When a Spinlock Is Appropriate

Spinlocks can work well when critical sections are very short and contention is low. They avoid kernel context switches, which can reduce latency in tight loops.

They are usually a bad choice when lock hold times are long, thread count is high, or single-core environments are common. In those cases, mutexes generally perform better and waste less CPU.

Basic C11 Spinlock with atomic_flag

C11 provides atomic_flag, a lock-free primitive designed for test-and-set style synchronization.

c
1#include <stdatomic.h>
2
3typedef struct {
4    atomic_flag flag;
5} spinlock_t;
6
7void spinlock_init(spinlock_t *lock) {
8    atomic_flag_clear(&lock->flag);
9}
10
11void spinlock_lock(spinlock_t *lock) {
12    while (atomic_flag_test_and_set_explicit(&lock->flag, memory_order_acquire)) {
13        ;
14    }
15}
16
17void spinlock_unlock(spinlock_t *lock) {
18    atomic_flag_clear_explicit(&lock->flag, memory_order_release);
19}

memory_order_acquire ensures reads and writes inside the critical section are not moved before lock acquisition. memory_order_release ensures updates become visible before lock release.

Add CPU-Friendly Backoff

A tight empty loop can saturate CPU. Add backoff or architecture hints in the wait loop.

c
1#include <stdatomic.h>
2#include <threads.h>
3
4void spinlock_lock_backoff(spinlock_t *lock) {
5    int spins = 0;
6    while (atomic_flag_test_and_set_explicit(&lock->flag, memory_order_acquire)) {
7        if (++spins % 1000 == 0) {
8            thrd_yield();
9        }
10    }
11}

thrd_yield gives scheduler flexibility under contention and can improve overall throughput.

Example Usage with Shared Counter

Use the lock to guard shared mutable state.

c
1#include <stdio.h>
2#include <threads.h>
3
4#define THREADS 4
5#define INCREMENTS 100000
6
7static spinlock_t lock;
8static int counter = 0;
9
10int worker(void *arg) {
11    (void)arg;
12    for (int i = 0; i < INCREMENTS; i++) {
13        spinlock_lock(&lock);
14        counter++;
15        spinlock_unlock(&lock);
16    }
17    return 0;
18}
19
20int main(void) {
21    thrd_t t[THREADS];
22    spinlock_init(&lock);
23
24    for (int i = 0; i < THREADS; i++) {
25        thrd_create(&t[i], worker, NULL);
26    }
27    for (int i = 0; i < THREADS; i++) {
28        thrd_join(t[i], NULL);
29    }
30
31    printf("counter=%d\n", counter);
32    return 0;
33}

Compile with a C11-capable compiler and threading support.

bash
cc -std=c11 -O2 spinlock_demo.c -o spinlock_demo -pthread

Correctness Notes

A spinlock must never be copied while in use. Keep one stable instance and share its address. Also ensure every lock acquisition has a matching unlock, including error paths.

Spinlocks are non-recursive by default. If a thread tries to lock the same spinlock twice, it deadlocks itself.

Performance Guidance

Measure with your real workload. A microbenchmark with short synthetic loops can be misleading.

  • Check contention level and lock hold time.
  • Compare against mtx_t from C11 threads.
  • Profile CPU utilization, not just wall-clock latency.

Often a standard mutex wins once critical sections include I/O, memory allocation, or cache-miss-heavy work.

Add Non-Blocking Try Lock

Sometimes callers should skip work if the lock is busy instead of spinning. A try-lock helper can improve responsiveness.

c
1#include <stdbool.h>
2
3bool spinlock_try_lock(spinlock_t *lock) {
4    return !atomic_flag_test_and_set_explicit(&lock->flag, memory_order_acquire);
5}

Callers can use this in polling loops or best-effort cache updates where blocking is unnecessary. Be careful not to create starvation patterns where one thread repeatedly wins and others never progress. If fairness matters, switch to a queue-based lock or OS mutex.

Common Pitfalls

  • Using relaxed memory order for lock and unlock operations, which can break visibility guarantees.
  • Spinning for long critical sections and wasting CPU cycles.
  • Forgetting backoff or yielding under high contention.
  • Using spinlocks in power-sensitive environments where busy wait is expensive.
  • Assuming spinlocks are always faster than mutexes.

Summary

  • C11 atomic_flag enables a compact and correct spinlock implementation.
  • Acquire and release ordering are essential for memory safety.
  • Add backoff or thrd_yield to reduce contention pressure.
  • Use spinlocks only for very short critical sections with low contention.
  • Benchmark against mutex-based alternatives before deciding.

Course illustration
Course illustration

All Rights Reserved.