写入相同值的CAS操作能否保证逻辑正确?能否替换为Load操作?
我们有这样一个场景:每次点击按钮都会发起网络请求,但只需要保留并使用最新请求的结果。为实现这个逻辑,我用原子对象记录请求版本,只有当结果对应的版本和当前版本一致时,才会使用该结果。
std::atomic<int> version = {0}; // 按钮点击处理函数 void OnClick(){ auto v = version.fetch_add(1, std::memory_order::acq_rel); // #1 std::thread([v](){ int current = v+1; auto r = network_request(current ); // #2 if(version.compare_exchange_strong(current, current, std::memory_order::release, std::memory_order::relaxed)){ // #3 use_req(r); } }); }
假设有两次按钮点击事件A和B,A发生在B之前:
- 如果A中的#3操作晚于B中的#1操作,A的请求结果会被忽略;
- 如果A中的#3操作早于B中的#1操作,A的CAS操作会成功,结果会被使用,随后B中的#1会读取A中#3写入的值,这也保证了A的网络请求happen-before于B的网络请求(假设
use_req是线程安全的)。
核心问题:
那么,写入相同值的CAS操作能否保证逻辑正确?这个CAS操作能不能替换成单纯的Load操作?
更新
我认为在检查网络请求期间版本是否更新时,纯Load操作和CAS操作存在本质差异:
使用CAS操作的情况
当#3使用CAS时,如果A的网络请求返回前触发了B,且B的fetch_add读取了A的#1产生的修改,那么A的#3必定会失败。这符合C++标准中关于原子读改写操作的规定:
原子读改写操作必须读取该操作的写入动作之前,修改顺序中的最后一个值。
这里的修改顺序必然是{A的fetch_add, B的fetch_add}。如果A的CAS操作成功,那作为读改写操作,它就违反了上述规定,所以这种场景下A的条件检查一定能检测到版本变化。
使用纯Load操作的情况
如果#3换成纯Load操作,哪怕B的#1已经读取了A的#1产生的修改,根据C++标准中关于数据竞争的规定:
如果原子对象M上的副作用X happen-before于M的值计算B,那么B的取值要么来自X,要么来自修改顺序中X之后的某个副作用Y。
此时修改顺序仍然是{A的fetch_add, B的fetch_add},但A的#3可能读取到A的#1产生的修改X(因为存在happen-before关系),也可能读取到X之后的B的fetch_add(即Y)。换句话说,纯Load操作无法保证一定能检测到这种场景下的版本变化。
这正是纯Load操作和CAS操作在版本变化检查中的核心差异:CAS作为原子读改写操作,会强制读取修改顺序中最新的值,确保不会错过后续的版本更新;而纯Load操作仅保证读取的值在修改顺序中不早于某个点,但可能读取到旧值,导致错误地认为版本没有更新,进而使用过期的请求结果。
所以写入相同值的CAS操作是逻辑正确的,它不能被纯Load操作替换。
内容的提问来源于stack exchange,提问作者xmh0511

