原子操作与并发(Atomics & Concurrency)

多处理器/多核下,普通读写可能被重排或撕裂,需用原子指令与内存屏障保证正确性。

LOCK 前缀

在以下指令前加 lock 前缀,使该指令在多核间原子执行(锁定总线/缓存行):

lock addl $1, (%rdi)        # 原子自增内存
lock xchg %eax, (%rdi)      # 原子交换(xchg 对内存默认隐含 lock)
lock cmpxchg %esi, (%rdi)   # 原子比较交换(CAS)
lock bts $3, (%rdi)         # 原子置位

lock 可与:add/sub/inc/dec/neg/notand/or/xorbtc/bts/btrxchgcmpxchg/cmpxchg8b/cmpxchg16bxadd

内存序要点:x86 上 lock 前缀指令本身就兼有 acquire/release 屏障语义,无锁结构(互斥、引用计数、无锁队列)大多以 lock 指令 + CAS 为基础构建。

原子交换(xchg)

xchg 本身即是原子的(即使不加 lock,对内存操作隐含 lock):

# 自旋锁加锁:把 1 写入锁,返回旧值
# rdi = &lock
acquire:
    movl $1, %eax
    xchgl %eax, (%rdi)
    testl %eax, %eax
    jnz acquire              # 旧值为 1 表示已被占用,自旋
    ret

原子比较并交换(CAS,cmpxchg)

cmpxchg src, dst:若 dst == AL/AX/EAX/RAX(累加器),则 dst = src,ZF=1;否则 RAX = dst,ZF=0。常用于无锁算法。

# 无锁自增:retry: old = *p; if CAS(p, old, old+1) break;
# rdi = ptr
lock_inc:
1:
    movl (%rdi), %eax        # eax = 旧值
    movl %eax, %ecx
    addl $1, %ecx            # ecx = 旧值+1
    lock cmpxchgl %ecx, (%rdi)   # 若 *p==eax 则 *p=ecx
    jne 1b                   # 失败(被别人改了)则重试
    ret

cmpxchg8b(8 字节,用 edx:eax 比较、ecx:ebx 交换)、cmpxchg16b(16 字节,x86-64)用于更大的原子量。

原子 Fetch-And-Add(xadd)

xadd src, dst:交换并相加,dst = dst + srcsrc = 旧dstlock xadd 实现 fetch_add:

movl $1, %eax
lock xaddl %eax, (%rdi)   # eax = 旧值, (*rdi) += 1

内存屏障(Memory Fence)

x86 具有较强内存模型(store 不会与 store 重排,load 不会与 load 重排),但仍需屏障:

指令 作用
mfence 所有读写全序(读写屏障)
lfence 读屏障
sfence 写屏障
lock 前缀指令 也隐含完整屏障语义
sfence                     # 保证前面的写先完成
movl $1, (%rdi)            # 发布数据
mfence
movl $1, (%rsi)            # 发布标志(StoreStore/StoreLoad 安全)

原子标志位操作

lock btsl $0, (%rdi)       # 原子测试并置位第 0 位,返回旧值到 CF
lock btsq $7, (%rdi)       # 64 位版本

自旋锁完整示例

# void spin_lock(int *l)  rdi=&l
spin_lock:
    movl $1, %eax
1:
    xchgl %eax, (%rdi)     # 隐含 lock 的原子交换
    testl %eax, %eax
    jz   2f                # 旧值为 0:抢锁成功
    pause                  # 自旋中让出流水线(HT 友好、省电)
    jmp  1b
2:
    ret

# void spin_unlock(int *l) rdi=&l
spin_unlock:
    movl $0, %eax
    xchgl %eax, (%rdi)
    ret

pause 降低自旋等待对兄弟超线程与内存序的影响,写自旋循环务必加上。