原子操作与并发(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/not、and/or/xor、btc/bts/btr、xchg、cmpxchg/cmpxchg8b/cmpxchg16b、xadd。
内存序要点: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 + src,src = 旧dst。lock 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降低自旋等待对兄弟超线程与内存序的影响,写自旋循环务必加上。