基于共享内存的并发带来的不确定性
线程之间共享内存 (shared mutable states) 时的不确定性:
- 线程间交错运行 (thread interleaving), 导致线程之间的读写指令被交错执行
- CPU 进行的指令重排 (reordering), 导致线程内部的读写指令被重新排布
线程间交错运行
线程间的交换运行, 是由操作系统内核的调度器 (scheduler) 控制的.
看一个例子:
#![allow(unused)] fn main() { static COUNTER = AtomicUsize::new(0); // thread A & B let c = COUNTER.load(); COUNTER.store(c + 1); }
问题, 线程A 和 B 交错运行带来的不确定性:
[COUNTER = 0]
Thread#A load()
Thread#B load()
Thread#A store()
Thread#B store()
[COUNTER = 1]
解决方案, 修改以上指令, 使用原子化的读写内存指令 (atomically reading & writing):
Read-modify-write
模式swap()
compare-and-swap()
fetch-and-add()
基于以上读写模式修改后的代码:
#![allow(unused)] fn main() { static COUNTER = AtomicUsize::new(0); // thread A & B let c = COUNTER.fetch_and_add(1); }
运行效果如下:
[COUNTER = 0]
Thread#A fetch_and_add()
Thread#B fetch_and_add()
[COUNTER = 2]
CPU 进行的指令重排
线程内读写指令的重新排布, 是 CPU 或者编译器为了优化指令的性能.
举个例子, 下面的代码片段中, FLAG
是在线程 A 和 B 之间共享的, 比如其类型是 AtomicI32
:
Thread# A || Thread# B
||
DATA = 42; || if FLAG.load() {
FLAG.store(1); || assert_eq!(DATA, 42);
|| }
问题, 两个线程的指令被执行时发生了重排:
- 线程 A 或者线程 B 发生指令重排后, 在线程 B 中的断言 (
assert_eq!()
) 表达式就会失败
解决方法, 禁止重排:
- 内存屏障 (memory barrier, fence), 在读写指令之间加上内存屏障
- 指定访存顺序 (access ordering)
FLAG.store(1, release)
FLAG.load(acquire)
指定访存顺序的方法, 使用 release/acquire 同步原语:
release store
, 禁止与前面的指令重排acquire load
, 禁止与后面的指令重排
Thread# A || Thread# B
||
DATA = 42; || if FLAG.load(acquire) {
FLAG.store(1, release); || assert_eq!(DATA, 42);
|| }
使用内存屏障的方法, 通过 顺序一致性 sequentially consistent (SC) 同步原语:
- SC 屏障: 禁止在它之前和在它之后的指令, 跨越它被重排, 可以保证在它之前的指令先被执行, 在它之后的指令后被执行
- 访存顺序为 relaxed, 就是说不要求显式地指定访存顺序, 它无关紧要
Thread# A || Thread# B
||
DATA = 42; || if FLAG.load(relaxed) {
fence(SC); || fence(SC);
FLAG.store(1, relaxed); || assert_eq!(DATA, 42);
|| }