Skip to content

Atomic counters

Level: 201 · anyone who has put a mutex around a single number

One line: An atomic.Int64 gives the same exact count a mutex gives, with no lock. CompareAndSwap lets exactly one of eight goroutines that close a server at the same moment do the shutdown.

Verified output of atomic_counters_go.go — regenerated by tools/run_examples.py, never hand-typed.

expected total:           800000
atomic.Int64 total:       800000
goroutines calling Close: 8
calls that shut it down:  1
calls told it was closed: 7
shutdowns performed:      1

Reading the output

The counter. Eight goroutines each call views.Add(1) 100,000 times, and the total is 800,000, the same as the mutex version gets. Add is read, add and write done as one step that no other goroutine can split. The sync/atomic documentation ↗ defines it as the atomic equivalent of *addr += delta. There is no lock to take or forget, and the zero value is 0, ready to use.

atomic.Int64 and its siblings (Bool, Int32, Uint32, Uint64, Uintptr and Pointer[T]) arrived in Go 1.19 ↗. Their release notes explain the point: the types hide the value, so every access has to go through the atomic methods. Before 1.19, you kept a plain int64 and called atomic.AddInt64(&n, 1). Nothing then stopped a later n++ somewhere else. Those functions still exist, and their documentation recommends the types as more ergonomic and less error-prone, especially on 32-bit platforms, where the functions leave 64-bit alignment to the caller.

The flag. Close calls closed.CompareAndSwap(false, true). In one atomic step that means: if the value is false, store true and report true; otherwise change nothing and report false. The start channel holds all eight goroutines back and releases them together, so they really do compete. Exactly one of them finds false. The other seven find the flag already set and return without shutting anything down. Which goroutine wins changes from run to run, so the program prints how many won, never which one.

The same method written with a plain bool would be two steps, a check and then a set: if !s.closed { s.closed = true; ... }. Two goroutines can both run the check before either runs the set, and then both shut the server down. That is a data race, and the race detector is how you find one. (The two-shutdown outcome is not machine-checked here.)

atomic_counters_go.go

atomic_counters_go.go in full — pasted here by tools/run_examples.py from the file CI runs.

// sync/atomic: an exact count with no lock, and a flag that exactly one of many
// goroutines can flip.
//
//  go build atomic_counters_go.go && ./atomic_counters_go
package main

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

const workers = 8

// server can be closed by any number of goroutines at once; it shuts down once.
type server struct {
    closed    atomic.Bool
    shutdowns atomic.Int64
}

// Close reports whether this call is the one that shut the server down.
func (s *server) Close() bool {
    if !s.closed.CompareAndSwap(false, true) {
        return false // somebody else got there first
    }
    s.shutdowns.Add(1)
    return true
}

func main() {
    // 1. A counter: Add is a read, an add and a write that nothing can split.
    const perWorker = 100_000
    var views atomic.Int64 // the zero value is 0, ready to use
    var wg sync.WaitGroup
    for range workers {
        wg.Go(func() {
            for range perWorker {
                views.Add(1)
            }
        })
    }
    wg.Wait()
    fmt.Printf("expected total:           %d\n", workers*perWorker)
    fmt.Printf("atomic.Int64 total:       %d\n", views.Load())

    // 2. First writer wins: every worker calls Close at the same moment.
    var srv server
    won := make([]bool, workers) // each goroutine writes only its own slot
    start := make(chan struct{})
    for i := range workers {
        wg.Go(func() {
            <-start
            won[i] = srv.Close()
        })
    }
    close(start) // release all of them together
    wg.Wait()

    winners := 0
    for _, w := range won {
        if w {
            winners++
        }
    }
    fmt.Printf("goroutines calling Close: %d\n", workers)
    fmt.Printf("calls that shut it down:  %d\n", winners)
    fmt.Printf("calls told it was closed: %d\n", workers-winners)
    fmt.Printf("shutdowns performed:      %d\n", srv.shutdowns.Load())
}

What to do

  • One number changed by one operation is a job for an atomic type: a counter, a gauge, or a flag that flips once. Use the types (atomic.Int64, atomic.Bool, ...) rather than the older AddInt64-style functions.
  • Use CompareAndSwap for "first one wins": a shutdown, a one-time claim, a state that may move only from A to B. Its bool result tells each caller whether it was the one.
  • Values that must agree need a mutex. Each atomic operation is atomic on its own. Two of them in a row are two separate moments, and another goroutine can run between them. The sync/atomic documentation says itself that, outside special low-level uses, synchronization is better done with channels or the sync package.
  • Don't copy one. Each type's documentation says it must not be copied after first use. Keep it in a struct you pass by pointer.

In other languages

  • The Concurrency library's chapter 02 Shared state (planned) will put atomics beside mutexes in six languages.
  • The Rust library's RwLock and atomics ↗ (an outline so far). Every Rust atomic operation takes an Ordering argument, from Relaxed to SeqCst. Go's take none: the Go 1.19 release notes ↗ say Go provides only sequentially consistent atomics. That page makes the same point as this one about two atomics not being atomic together.
  • The Rust library's Sharing across threads: Arc ↗. In Rust, a counter handed to thread::spawn goes behind an Arc, the atomically counted pointer, so that each thread owns a share of it. A Go closure simply captures the variable, and the spec ↗ says a captured variable lives as long as anything can still reach it.

Sources

  • sync/atomic: the definitions of add and compare-and-swap, the typed values, and the notes recommending them over the functions.
  • Go 1.19 release notes ↗: the new atomic types, and the revised memory model.
  • The Go Memory Model ↗: what "synchronizes before" means for atomic operations.
  • Jon Bodner, Learning Go, 2nd ed., chapter 12 "Concurrency in Go", the section on atomics. Burak Serdar, Effective Concurrency in Go, chapter 9 "Atomic Memory Operations". James Cutajar, Learn Concurrent Programming with Go, chapter 12 "Atomics, spin locks, and futexes".