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

mfence

full barrier; serialise loads and stores

lfence

load fence; serialise loads

sfence

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.

References#