Every language
12 implementations, copy-ready. One at a time with syntax highlighting, or all inline.
JSJavaScript
// single-threaded JS: this is ALWAYS safe — awaits interleave, threads don't:
let n = 0;
async function handler() {
n++; // no other thread can touch n between load and store
await persist(n);
}
// the multi-worker form — Atomics over a SharedArrayBuffer, not a number:
const shared = new SharedArrayBuffer(4);
const counter = new Int32Array(shared); // ship `shared` to each worker
// in each worker: Atomics.add(counter, 0, 1)
const total = Atomics.load(counter, 0); // sees every committed addJS is single-threaded, so n++ across awaits cannot tear — the trap is assuming workers are too. Atomics.add works only on a SharedArrayBuffer view, never a plain number (postMessage copies; only the shared buffer aliases), and it returns the pre-increment value.
TSTypeScript
const shared = new SharedArrayBuffer(Int32Array.BYTES_PER_ELEMENT);
const counter = new Int32Array(shared); // Atomics only works on these views
function hit(): number {
return Atomics.add(counter, 0, 1) + 1; // returns the PRE-increment value
}
function total(): number {
return Atomics.load(counter, 0);
}
// the futex side — block until another thread writes the cell:
// Atomics.wait(counter, 0, oldValue, timeoutMs) // Worker threads only
// Atomics.notify(counter, 0, 1) // wake one waiterThe Int32Array-over-shared-memory view IS the type contract — Atomics on anything else fails to compile. Atomics.wait blocks its thread, so it is legal in a Worker and throws on the main thread; notify is how another worker wakes it.
GoGo
import "sync/atomic"
type Counter struct {
n atomic.Int64 // Go 1.19+ type form — zero value works, no init
}
func (c *Counter) Inc() int64 { return c.n.Add(1) } // atomic add
func (c *Counter) Get() int64 { return c.n.Load() } // readers Load too
// pre-1.19 field spelling, still everywhere:
// var n int64
// atomic.AddInt64(&n, 1) // writers
// atomic.LoadInt64(&n) // readersatomic.Int64 is the Go 1.19+ type form — zero-value usable, no constructor, but never copy it (pass *Counter). Readers must Load too: a plain read racing an Add is a data race even when counts look right; when counter and flag change together, that is mutex territory.
RsRust
use std::sync::atomic::{AtomicU64, Ordering};
let n = AtomicU64::new(0);
// pure counting: Relaxed — the add is atomic, nothing else is ordered
let before = n.fetch_add(1, Ordering::Relaxed);
let total = n.load(Ordering::Relaxed);
// the counter-as-flag case — order it against OTHER memory:
n.store(42, Ordering::Release); // producer publishes
let v = n.load(Ordering::Acquire); // consumer sees everything before itRelaxed keeps the increment atomic but orders nothing else — exactly right for a pure total; reach for Release/Acquire (or SeqCst) when other variables must be seen consistently around the counter, the flag case. fetch_add returns the previous value.
PHPPHP
// PHP workers share NO memory — two requests never race a PHP variable.
// The race lives in the shared store behind them. Atomic ops belong there:
$redis = new Redis();
$redis->connect('127.0.0.1', 6379);
$hits = $redis->incr('page:hits'); // one atomic server-side op
// APCu — shared memory across FPM workers on one machine:
$hits = apcu_inc('page:hits');
// the trap: read-modify-write from PHP is two round trips and loses counts:
// $n = $redis->get('page:hits'); $n++; $redis->set('page:hits', $n); // NONo shared memory between requests means the race is in the store, not the script — INCR/apcu_inc run atomically where the data lives. Get-then-set from PHP is two round trips and loses counts under concurrency, the same lost-update as C's counter++.
PyPython
import threading
count = 0
lock = threading.Lock()
def hit() -> int:
global count
with lock: # += is load/add/store — the GIL does not fuse it
count += 1
return count
# when you only need unique ids, not a shared total — skip the cell:
# from itertools import count
# ids = count() # next(ids) is atomic; nothing shared to guardThe GIL makes += ALMOST safe — a thread can yield between the load and the store — and free-threaded CPython (3.13+ no-GIL builds) deletes the 'almost', making the lock non-optional. itertools.count() removes the shared cell entirely when unique ids are all you need.
C#C#
using System.Threading;
long hits = 0;
long ticket = Interlocked.Increment(ref hits); // returns the new value
long total = Volatile.Read(ref hits);
// the raw CAS every "update only if unchanged" loop is built on:
long seen, next;
do {
seen = Volatile.Read(ref hits);
next = seen + 100;
} while (Interlocked.CompareExchange(ref hits, next, seen) != seen);int++/long++ is never atomic in .NET — Interlocked is the lock-free primitive, and CompareExchange is the raw CAS behind every check-and-update loop (it returns the original value: != seen means you lost the race, retry). On 32-bit runtimes use Interlocked.Read for tear-proof 64-bit loads.
JvJava
import java.util.concurrent.atomic.AtomicLong;
AtomicLong hits = new AtomicLong();
long ticket = hits.incrementAndGet(); // the atomic ++ — returns the NEW value
long total = hits.get();
// high-contention variant — striped cells, summed only on read:
// import java.util.concurrent.atomic.LongAdder;
// LongAdder adder = new LongAdder();
// adder.increment();
// adder.sum();The JVM makes ++ atomic only on the Atomic* types — a plain long increment races. incrementAndGet vs getAndIncrement is new-value vs old-value. LongAdder stripes the count across cells and sums on read: built for many threads hammering one counter.
SwSwift
// Swift has NO atomic type — OSAtomicIncrement is deprecated. Actors
// serialize access instead; a locked class covers non-async contexts.
actor Counter {
private var n = 0
func increment() -> Int { n += 1; return n } // serialized by the actor
var value: Int { n }
}
// let t = await counter.increment()
final class LockedCounter { // when you cannot await
private let lock = NSLock()
private var n = 0
func increment() -> Int {
lock.lock(); defer { lock.unlock() }
n += 1; return n
}
}Swift ships no atomic language type and OSAtomicIncrement is deprecated — the language answer is an actor, which serializes every access by construction (each hop is an await). When await is impossible, the manual form is a class with NSLock or os_unfair_lock.
KtKotlin
import java.util.concurrent.atomic.AtomicLong
val hits = AtomicLong()
val ticket = hits.incrementAndGet() // JVM: the same AtomicLong as Java
val total = hits.get()
// multiplatform code that cannot touch java.util.concurrent:
// import kotlin.concurrent.atomic.AtomicInt // Kotlin 2.1+, experimental
// val n = AtomicInt(0)
// n.increment(); n.load()
// kotlinx.atomicfu is the established delegate-based alternativeSame JVM contract as Java — AtomicLong for a shared counter, LongAdder under heavy contention. kotlin.concurrent.atomic (and kotlinx.atomicfu) exist for multiplatform code where java.util.concurrent is off-limits; on the JVM they lower to the same instructions.
RbRuby
require 'thread'
class Counter
LOCK = Mutex.new
def self.hit
LOCK.synchronize { @@count += 1 } # class vars are the shared cell
end
def self.total
LOCK.synchronize { @@count }
end
end
# lock-free spelling — concurrent-ruby wraps the CAS loop:
# require 'concurrent-ruby'
# hits = Concurrent::AtomicFixnum.new
# hits.increment # -> the new value; hits.value to readMRI's GVL is the same almost-safe illusion as Python's GIL — and JRuby/TruffleRuby have no GVL at all. synchronize is the guarantee; Concurrent::AtomicFixnum wraps the CAS loop for you. @@ class variables are the classic shared cell nobody guards.
ZigZig
const std = @import("std");
var counter = std.atomic.Value(u64).init(0);
pub fn hit() void {
_ = counter.fetchAdd(1, .monotonic); // returns the old value
}
pub fn total() u64 {
return counter.load(.monotonic); // readers load atomically too
}
// the builtin underneath it all:
// _ = @atomicRmw(u64, &cell, .Add, 1, .monotonic);
// plain `cell +%= 1` on a shared cell is a data race — UB, like Cstd.atomic.Value(u64) is the typed cell — plain +%= on a shared variable is a data race, undefined behavior in Zig exactly as in C. Ordering is explicit at every call site: .monotonic for pure counting, .seq_cst when other loads/stores must order against it; @atomicRmw is the builtin layer underneath.