基于相同修改序判断原子Load与写原值CAS操作的功能等价性是否合理?
先从你的示例代码和问题背景说起:
原代码与行为分析
首先看你给出的原示例代码:
#include <atomic> #include <iostream> #include <thread> std::atomic<int> canceller = {0}; int main() { auto t1 = std::thread([]() { auto v = canceller.fetch_add(1, std::memory_order::relaxed); // #0 std::thread([v]() { int current = v + 1; if (canceller.load(std::memory_order::relaxed)==current) { // #1 std::cout<<"invoked, not canceled"; // #2 } }).join(); }); auto t2 = std::thread([]() { int current = 1; while(!canceller.compare_exchange_strong(current, 2, std::memory_order::relaxed,std::memory_order::relaxed)){ // #3 current = 1; } }); t1.join(); t2.join(); }
根据C++内存模型的标准条款:
根据[intro.races] p14:如果原子对象M上的副作用X happens before M的值计算B,那么B的取值来自X,或者来自M的修改序中X之后的副作用Y。
这里#0 happens before #1,所以#1的load可以读到#0的结果(值1)或者#3的结果(值2),对应两种合法行为:
- 读到#0的1:
1 == 1为真,执行#2打印 - 读到#3的2:
2 == 1为假,不执行#2
这两种情况在修改序0 < #0 < #3下都是允许的。
修改后的CAS版本与核心疑问
你把#1的load改成了写原值的CAS操作:
#include <atomic> #include <iostream> #include <thread> std::atomic<int> canceller = {0}; int main() { auto t1 = std::thread([]() { auto v = canceller.fetch_add(1, std::memory_order::relaxed); // #0 std::thread([v]() { int current = v + 1; if (canceller.compare_exchange_strong( current, current, std::memory_order::relaxed, std::memory_order::relaxed)) { // #1(修改为CAS) std::cout<<"invoked, not canceled"; // #2 } }).join(); }); auto t2 = std::thread([]() { int current = 1; while(!canceller.compare_exchange_strong(current, 2, std::memory_order::relaxed,std::memory_order::relaxed)){ // #3 current = 1; } }); t1.join(); t2.join(); }
你的核心疑问是:能不能通过「假设CAS和原load在相同修改序下行为一致」来判断两者功能等价?这种分析方法是否有效?
核心解答
1. 单一修改序的分析方法并不充分
功能等价性要求两个程序的所有合法执行路径的可观察行为完全一致,而单一修改序只代表某一种具体执行的原子变量修改顺序,无法覆盖所有可能的合法执行场景。
更关键的是:CAS是**读-改-写(RMW)**原子操作,而load是单纯的读操作,两者的语义约束不同——CAS的读和写是原子绑定的,而load是独立的读操作,不能直接套用load的行为来推导CAS的行为。
2. 这个特定场景下,CAS与原load是功能等价的
先明确这个场景下CAS的语义:compare_exchange_strong(current, current, ...)中,current是v+1=1,它的行为是原子地检查原子变量当前值是否等于1,如果是则返回true(并将原子变量设为1,相当于无修改),否则返回false(并将current更新为原子变量的当前值)。
结合这个场景的唯一可能的修改序0 < #0 < #3(因为#3的CAS只有当原子变量值为1时才会成功,而#0的fetch_add是把原子变量从0改成1的唯一操作,所以#0必须在#3之前),我们分析所有合法执行路径:
- 情况1:#1的执行在#3之前
- 原
load版本:load读到#0的结果1,1 == 1为真,执行#2打印 - CAS版本:CAS原子检查到原子变量值为1,返回true,执行#2打印
两者可观察行为一致。
- 原
- 情况2:#1的执行在#3之后
- 原
load版本:load读到#3的结果2,2 == 1为假,不执行#2 - CAS版本:CAS原子检查到原子变量值为2,返回false,不执行#2
两者可观察行为一致。
- 原
因此在所有合法执行路径下,两个版本的可观察行为完全一致,这两个版本是功能等价的。
你之前的分析中误以为CAS会打破等价性,可能是误解了CAS的语义——在这个特定场景下,写原值的CAS的可观察行为和load() == current完全一致。
3. 正确的功能等价性判断方法
要判断两个原子操作相关的程序是否功能等价,应该遵循以下步骤:
- 枚举所有合法执行路径:包括所有可能的修改序、线程调度顺序(基于C++内存模型的约束,比如happens before关系、原子操作的语义)。
- 检查每个路径的可观察行为:可观察行为包括打印、IO操作、原子变量的最终值、其他线程可见的状态变化等。
- 验证一致性:如果所有合法执行路径的可观察行为都完全一致,那么两个程序是功能等价的;否则不是。
额外补充:什么时候CAS与load的行为会不等价?
如果你的场景稍有变化,比如原代码的逻辑是load后用读到的值做其他操作,或者CAS的新值与期望值不同,两者的行为就会完全不同。例如:
- 原代码如果是
int val = canceller.load(); if (val == current && val == canceller.load())(两次load),那么CAS版本无法直接等价,因为CAS的原子性可以避免两次load之间的竞态。 - 如果CAS的新值不是期望值(比如
compare_exchange_strong(current, 3)),那么CAS会修改原子变量的值,这和单纯的load的行为完全不同。
引用标准的关键条款
根据[atomics.order] p10:原子读-改-写操作必须读取修改序中在它的写操作之前的最后一个值。
这个条款是分析RMW操作行为的核心依据,要注意结合操作的具体语义(读 vs RMW)来应用。




