Rust中Ordering::SeqCst内存序作用及案例疑问
一、计数器案例:Release/Acquire为何出现0结果,SeqCst为何能避免
Release/Acquire版本的0结果成因
Release和Acquire内存序仅保证特定线程对之间的单向同步:
- Thread_a的
a.store(true, Release)与Thread_1的a.load(Acquire)之间存在同步关系,Thread_1在看到a=true后,能确保Thread_a中所有在store之前的操作都已完成。 - 但Thread_1后续的
b.load(Relaxed)和Thread_b的b.store(true, Release)之间没有同步约束——CPU或编译器可能对操作重排序,或者缓存未及时同步,导致Thread_1看到a=true时,还看不到Thread_b写入的b=true。
同理,Thread_2看到b=true时,也可能看不到Thread_a写入的a=true。当这两种情况同时发生时,两个线程都不会执行counter.fetch_add,最终counter结果就是0。
SeqCst版本避免0结果的原因
SeqCst除了具备Release/Acquire的同步能力外,核心特性是所有标记为SeqCst的原子操作存在一个全局统一的单总修改顺序。也就是说:
- Thread_a的
a.store和Thread_b的b.store这两个SeqCst操作,在全局顺序里必然有先后之分:- 若
a.store先于b.store:当Thread_1看到a=true时,b.store已经在全局顺序中完成,Thread_1的b.load(SeqCst)必然能看到true,会执行counter加1; - 若
b.store先于a.store:Thread_2看到b=true时,a.store已经完成,Thread_2的a.load(SeqCst)必然能看到true,会执行counter加1。
- 若
无论哪种情况,至少有一个线程会对counter加1,因此0结果不可能出现。
二、字符串修改案例:Release/Acquire版本与SeqCst版本效果不一致
SeqCst版本的安全逻辑
原SeqCst版本中,A.store和B.store属于SeqCst操作,存在全局单总顺序:
- 若
A.store先执行:线程b加载A.load(SeqCst)会看到true,不会进入修改S的分支;线程a加载B.load(SeqCst)会看到false,进入修改分支。 - 若
B.store先执行:线程a加载B.load(SeqCst)会看到true,不修改S;线程b加载A.load(SeqCst)会看到false,进入修改分支。
两种情况都只会有一个线程修改S,不会出现并发修改的未定义行为。
Release/Acquire版本的风险
换成Release/Acquire后,失去了全局单总顺序的约束:
- 线程a执行
A.store(Release)后,线程b执行B.store(Release),此时两个store操作之间没有同步关系。 - 线程a加载
B.load(Acquire)时,可能还看不到B=true;同时线程b加载A.load(Acquire)时,也可能看不到A=true。 - 这种情况下,两个线程都会进入修改
S的分支,导致对S的并发写入——这属于未定义行为,可能引发内存损坏、程序崩溃等问题。
因此Release/Acquire版本无法保证和原SeqCst版本一致的安全效果。
三、理解SeqCst的“单总修改顺序”
“单总修改顺序”是指:所有标记为SeqCst的原子写操作(包括store、fetch_add等),在整个程序中存在一个唯一的、所有线程都认可的执行顺序;每个SeqCst读操作都会观察到这个顺序中最后一个对目标变量的写操作。
举个具体例子:
假设有两个原子变量:X: AtomicUsize = AtomicUsize::new(0)、Y: AtomicUsize = AtomicUsize::new(0),四个线程执行以下操作:
- 线程1:
X.store(1, SeqCst) - 线程2:
Y.store(2, SeqCst) - 线程3:
let x = X.load(SeqCst); let y = Y.load(SeqCst); - 线程4:
let y = Y.load(SeqCst); let x = X.load(SeqCst);
在SeqCst约束下,全局顺序要么是X.store先于Y.store,要么是Y.store先于X.store:
- 如果是
X.store先:线程3会读到x=1, y=2,线程4也会读到y=2, x=1; - 如果是
Y.store先:线程3会读到x=1, y=2,线程4同样会读到y=2, x=1。
绝不会出现“线程3读到X=1但Y=0,同时线程4读到Y=2但X=0”的情况——因为SeqCst强制所有线程对这些写操作的顺序看法完全一致。而如果用Release/Acquire,就可能出现这种“各说各话”的矛盾场景,因为它没有全局统一的顺序约束。
内容的提问来源于stack exchange,提问作者zexed640

