Skip to content

Linux Synchronization Primitive Selection

Purpose

This note provides practical guidance for selecting synchronization primitives in Linux.

It focuses on:

  • synchronization goals
  • execution constraints
  • performance characteristics
  • common tradeoffs

This note does not explain API details.

Refer to API references for syntax.


Synchronization Goals

Synchronization usually solves one or more of the following problems.

Goal Description
Mutual exclusion Prevent concurrent modification
Atomic update Guarantee single operation correctness
Visibility Ensure updates become observable
Ordering Control operation ordering
Waiting Suspend until condition becomes true
Throughput Reduce synchronization overhead

Choosing the correct primitive starts from identifying the actual goal.


Primitive Overview

Primitive Protect Multiple Fields Sleep Allowed IRQ Safe Ordering Waiting
mutex Yes Yes No Yes No
spinlock Yes No Yes Yes No
atomic No No Yes Partial No
READ_ONCE / WRITE_ONCE No Yes Yes No No
condition variable No Yes No Through mutex Yes
wait queue No Context dependent Yes Through wakeup Yes

Selection Workflow

Need synchronization?

├── No
│   └── No synchronization
└── Yes
    ├── Need to wait?
    │     └── condition / wait queue
    ├── Multiple fields?
    │     └── mutex
    ├── IRQ context?
    │     └── spinlock
    ├── Single shared value?
    │     └── atomic
    └── High-performance lock-free?
          └── atomic + memory ordering

Primitive Selection

Mutex

Use when:

  • protecting complex state
  • operation may sleep
  • consistency is more important than latency

Typical examples:

  • I2C transaction
  • SPI transaction
  • cache update
  • application state

Characteristics:

simple
safe
sleepable

Default recommendation.


Spinlock

Use when:

  • critical section is short
  • sleep is forbidden
  • interrupt path

Typical examples:

  • IRQ event queue
  • ring metadata
  • scheduler internals

Characteristics:

low latency
busy wait
non-sleepable

Avoid long holding time.


Atomic

Use when:

  • operating on one logical value
  • lock overhead dominates

Typical examples:

  • counters
  • state machine
  • ready flag
  • reference count

Characteristics:

very fast
limited scope

Avoid using multiple atomics to emulate structure locking.


READ_ONCE / WRITE_ONCE

Use when:

  • prevent compiler optimization
  • publish simple state

Typical examples:

  • stop flag
  • polling loop
  • statistics

Characteristics:

visibility helper
not synchronization

Does NOT replace mutex or atomic ordering.


Memory Ordering

Use when:

  • synchronization without lock
  • visibility matters

Typical examples:

  • lock-free queue
  • state publish
  • internal synchronization

Examples:

acquire
release
full barrier

Usually hidden inside higher-level primitives.


Practical Examples

Scenario Recommendation
Userspace threads pthread_mutex
Kernel worker thread mutex
IRQ handler spinlock
Shared counter atomic
Ready flag atomic + release/acquire
Event queue mutex
Ring buffer spinlock
Shared memory queue mutex + condition
Statistics atomic
Sensor cache mutex
Pollable driver wait queue
Reference counter atomic

Rules of Thumb

Rule 1

Prefer mutex first.

Rule 2

Use spinlock only when sleeping is impossible.

Rule 3

Use atomic only for one logical state.

Rule 4

Avoid lock-free until profiling proves necessity.

Rule 5

Correctness

Maintainability


Performance

Common Mistakes

Wrong:

Use atomic to protect structure

Wrong:

Hold spinlock during I2C access

Wrong:

Use READ_ONCE as synchronization

Wrong:

Optimize before measuring

Related Notes

  • atomics-memory-ordering.md
  • pthread-condition-variable.md
  • futex-foundation.md

Related APIs

Userspace:

  • pthread_mutex
  • pthread_cond
  • stdatomic

Kernel:

  • mutex
  • spinlock
  • atomic_t
  • READ_ONCE
  • wait_queue