Skip to article
ALGORITHMICSConcurrency
Concurrency6 min read

Atomics

Indivisible operations in hardware — and the loop that turns one into any update you like.


count++ is three machine operations, and two threads can interleave them and lose an update. A mutex fixes that by excluding everyone else.

For a single counter, that is a lot of machinery. The CPU can do better.

One instruction, indivisible

Processors provide instructions that read and write in a single step nothing can interleave with:

counter.fetchAdd(1); // read, add, write — atomically

No lock, no waiting, no kernel involvement. The hardware guarantees that no other core observes a half-finished state.

Compare-and-swap is the general one

fetchAdd handles addition. For anything else there is one primitive that handles everything:

compare-and-swap(expected, next) — if the value is still expected, write next and report success. Otherwise change nothing and report failure.

Wrap it in a retry loop and you can apply any update at all:

1 / 3

in memory

7

I expected

7

I want to write

8

do {
  expected = counter.load();
  next = expected + 1;
} while (!counter.compareExchange(expected, next));

✗ rejected

Read 7, planned to write 8 — but another thread got there first and memory is now 9.

function increment(counter: Atomic<number>) {
let expected: number;
let next: number;
do {
expected = counter.load();
next = expected + 1; // any computation you like goes here
} while (!counter.compareExchange(expected, next));
}

The ABA problem

CAS asks “is it still what I expected?” — and cannot tell “unchanged” from “changed and changed back”.

Thread 1: reads A, gets descheduled
Thread 2: changes A → B → A
Thread 1: CAS(A, next) succeeds ← but the world moved underneath it

For a counter this is harmless; the value is all that matters. For a pointer it is a memory-corruption bug: the node you read may have been freed and its address reused for something else entirely.

The standard fixes:

When atomics beat a lock, and when they do not

Better for a single word: counters, flags, one pointer swap. No syscall, no descheduling, and no risk of deadlock.

Worse under heavy contention. Every failed CAS is wasted work and a cache line bounced between cores. A mutex under contention parks the loser, which uses no CPU; a CAS loop burns it. Past some contention level the lock wins, and the crossover is lower than most people assume.

Impossible for anything spanning more than one word. Two accounts cannot be updated atomically by CAS. That needs a lock, or a transaction, or a redesign.

In JavaScript

The main thread is single-threaded, so most code never needs any of this. With Web Workers and a SharedArrayBuffer it becomes real:

const shared = new Int32Array(new SharedArrayBuffer(4));
Atomics.add(shared, 0, 1);
Atomics.compareExchange(shared, 0, expected, next);
Atomics.wait(shared, 0, 0); // block until it changes — workers only

SharedArrayBuffer requires cross-origin isolation headers, which is the Spectre mitigation and is why it is not available by default.