Concurrency#
At the assembly level concurrency is atomics, fences,
and kernel syscalls (clone, futex, signal
handlers). Do not usually write multi-threaded
applications in pure assembly; they read the atomic /
fence / lock instructions emitted by C / C++ / Rust compilers
and write small primitives for lock-free counters, spinlocks,
and ring buffers.
The lock prefix#
x86-64’s lock prefix makes the next read-modify-write
instruction atomic. The CPU asserts a bus lock (or, more
commonly, takes a cache line in exclusive state) for the
duration.
lock add qword [rdi], 1 ; atomic increment
lock inc qword [rdi] ; same; LOCK INC
lock xadd qword [rdi], rax ; rax = old [rdi]; [rdi] += old rax
lock sub qword [rdi], 1 ; atomic decrement
; CAS: compare-and-swap
; if [rdi] == rax then [rdi] = rcx; sets ZF
lock cmpxchg qword [rdi], rcx
xchg is implicitly locked when one operand is memory;
do not write lock xchg (the prefix is
redundant).
xchg qword [rdi], rax ; atomic swap
Atomic counters#
A lock-free shared counter.
; void inc(uint64_t *counter)
inc_counter:
lock add qword [rdi], 1
ret
; uint64_t fetch_inc(uint64_t *counter)
fetch_inc:
mov rax, 1
lock xadd qword [rdi], rax ; rax = old value
ret
Spinlock#
A minimal test-and-set spinlock.
; lock byte: 0 = free, 1 = held
; void spin_lock(volatile uint8_t *l)
spin_lock:
.retry:
mov al, 1
xchg byte [rdi], al ; atomic swap; al = old value
test al, al
jz .got_it
pause ; hint to CPU: spinning
jmp .retry
.got_it:
ret
; void spin_unlock(volatile uint8_t *l)
spin_unlock:
mov byte [rdi], 0 ; plain write; release
ret
pause tells the CPU “I am in a spin loop”; saves power and
avoids memory-ordering penalties on the rival.
Compare-and-swap (CAS)#
The building block of every lock-free algorithm.
; bool cas(uint64_t *p, uint64_t expected, uint64_t new)
cas:
mov rax, rsi ; expected
lock cmpxchg qword [rdi], rdx
sete al ; 1 if CAS succeeded
movzx eax, al
ret
Use: a lock-free push.
; void push(node_t **head, node_t *n)
push_lockfree:
.retry:
mov rax, qword [rdi] ; head
mov qword [rsi], rax ; n->next = head
lock cmpxchg qword [rdi], rsi
jne .retry
ret
Fences#
Memory barriers that enforce ordering between loads and stores.
Instruction |
Effect |
|---|---|
|
full barrier; serialise loads and stores |
|
load fence; serialise loads |
|
store fence; serialise stores |
x86-64 is strongly ordered: most loads and stores are
already ordered with respect to each other. The operator
inserts mfence only when reordering an explicit non-temporal
store, after writing GPU-mapped memory, or between paired
movnt and a subsequent normal load.
ARM64 is weakly ordered; use dmb (Data Memory
Barrier) with explicit options.
dmb ish // full barrier between CPU cores
dmb ishld // load fence
dmb ishst // store fence
ARM64 atomics (LL / SC)#
Pre-ARMv8.1, ARM64 atomics use load-linked / store-conditional pairs.
// atomic increment
loop:
ldxr x0, [x1] // load-exclusive
add x0, x0, #1
stxr w2, x0, [x1] // store-exclusive; w2 = 0 on success
cbnz w2, loop
ARMv8.1+ adds dedicated LSE atomics (ldadd, swp,
cas):
// atomic increment, LSE
mov x0, #1
ldadd x0, x0, [x1] // x0 = old; [x1] += 1
futex#
The Linux kernel primitive for sleep-on-address / wake. The
glibc pthread_mutex_t is built from atomic operations plus
futex(FUTEX_WAIT, ...) when the lock is contended.
; futex(addr, FUTEX_WAIT, expected, NULL, NULL, 0)
mov rax, 202 ; syscall: futex
; rdi = uaddr, rsi = futex_op, rdx = val, r10 = timeout, r8 = uaddr2, r9 = val3
syscall
The typical pattern: spin a few times, then futex
to sleep; another thread does the atomic update and calls
futex(FUTEX_WAKE).
rdtsc and timing#
rdtsc reads the timestamp counter (64-bit cycle counter
split across edx:eax). Used for fine-grained timing and
side-channel analysis.
rdtsc
shl rdx, 32
or rax, rdx ; rax = full 64-bit timestamp
rdtscp adds an implicit mfence and reads a CPU
identifier into rcx; better for cross-core timing.
Signals#
A signal handler runs in the same process but can interrupt at
any instruction. The operator only reads / writes
sig_atomic_t-typed variables from a handler; everything
else risks torn reads.
x86-64 Linux signal delivery uses rt_sigaction (syscall
13). The kernel pushes the user context onto the signal stack
and jumps to the handler; the operator returns through the
rt_sigreturn syscall, which restores the context.