Atomics Complete

Updated

September 8, 2026

Atomics Complete

Overview

sync/atomic provides lock-free operations that the CPU can perform safely under concurrency. Composition of atomics is not atomic—that distinction separates counters from broken “clever” state machines.

Non-atomic increment

total++ // read-modify-write: can lose updates
// 5 Gs × 10000 → often total < 50000
G1 reads 42
G2 reads 42
G1 writes 43
G2 writes 43   // lost update

Atomic types and ops

Type Ops
atomic.Bool Load, Store, Swap, CompareAndSwap
atomic.Int32/64, Uint… + Add, And, Or (1.23+)
atomic.Pointer[T] typed pointer
atomic.Value any type (consistent concrete type)
var total atomic.Int32
total.Add(1)
fmt.Println(total.Load())

// CAS
var flag atomic.Bool
if flag.CompareAndSwap(false, true) {
    // first closer wins
}

Pass atomics by pointer (or embed in structs used via pointer).

Composition traps

// Guaranteed sum of +1/+1 even with sleep between (sequence-independent Adds)
counter.Add(1)
sleep()
counter.Add(1)

// NOT a reliable algorithm: Load then branch then Add
if counter.Load()%2 == 0 {
    counter.Add(1)
} else {
    counter.Add(2)
}
// No data race, but race CONDITION → unpredictable result
regions: OK | Race condition without data race
flow:
  [Br]
       |
       v
  [Ad]

Bulletproof composite update: mutex around the whole sequence, or a careful CAS loop that retries.

CAS gate (mutex alternative for once)

type Gate struct{ closed atomic.Bool }

func (g *Gate) Close() {
    if !g.closed.CompareAndSwap(false, true) {
        return // already closed
    }
    // free resources once
}

Great for “first caller wins / early exit”. Not a substitute when waiters must block until unlock.

When to use atomics

Use Prefer
Counters, flags atomics
Multi-field invariants mutex
Hand-off of ownership channel
Complex lock-free structures expert only + heavy tests

Runnable example

go run -race .
package main

import (
    "fmt"
    "sync"
    "sync/atomic"
)

func main() {
    var total atomic.Int32
    var wg sync.WaitGroup
    for i := 0; i < 5; i++ {
        wg.Go(func() {
            for j := 0; j < 10000; j++ {
                total.Add(1)
            }
        })
    }
    wg.Wait()
    fmt.Println("total", total.Load())
}

Expected: total 50000 and race-clean.

Try next: Implement a CAS loop that doubles a value only if still equal to an observed snapshot.