纯Load+Store能否模拟RMW操作?原子操作顺序相关问询
在C++程序中,若对某原子对象的所有操作均为**RMW(读-改-写)**操作,根据[atomic.order] p10规则,可保证该原子对象修改顺序中的某一修改仅会被一个RMW操作读取,基于此可构建临界区。
示例代码:
#include <iostream> #include <thread> #include <atomic> int main(){ std::atomic<int> state = {0}; int protect_v = 0; std::thread t1([&](){ int expected = 0; if(state.compare_exchange_strong(expected,1,std::memory_order::release,std::memory_order::acquire)){ protect_v = 2; }else{ std::cout<< "t1 read: "<<protect_v <<"\n"; } }); std::thread t2([&](){ int expected = 0; if(state.compare_exchange_strong(expected,1,std::memory_order::release,std::memory_order::acquire)){ protect_v = 3; }else{ std::cout<< "t2 read: "<<protect_v <<"\n"; } }); t1.join(); t2.join(); }
但若将上述程序中的任一RMW操作替换为Load操作后紧跟Store操作,该保证是否依然成立?在此情况下,若Load读取到0且Store写入1,即便这两个操作间无干扰,另一RMW操作仍可能读取到0,反之亦然,这是否会打破原有保证?
Q1:若要让单个原子对象修改顺序中的某一修改仅被一个操作读取,是否必须对该对象的所有操作均为RMW操作,以确保它们竞争读取该值?
是的,必须所有操作都是RMW操作才能保证这一点。
RMW操作是原子性的整体操作——读取当前值、计算新值、写入新值的过程不可分割。当多个RMW操作竞争同一个原子对象时,每个RMW操作要么成功修改对象(读取到的是修改顺序中前一个未被其他RMW操作“抢占”的值),要么读取到其他RMW操作已经写入的新值,不会出现两个操作同时读取到同一个旧值并都进行写入的情况。
如果混合使用非RMW操作(比如单独的Load+Store),Load和Store是两个独立的原子操作,中间存在时间窗口:线程A执行Load读到0后,线程B的RMW操作可能先完成写入1,此时线程A再执行Store写入1,就会出现两个操作(线程A的Load、线程B的RMW)都读取到了初始的0值,打破了“一个修改仅被一个操作读取”的保证。
Q2:给定如下代码:
std::atomic<bool> flag = false; std::atomic<int> state = 0; // thread 1: if(state.load(std::memory_order::seq_cst)==0){ state.store(1,std::memory_order::seq_cst); flag.store(true,std::memory_order::seq_cst); } // thread 2: while(!flag.load(std::memory_order::relaxed)){} state.exchange(1,std::memory_order::seq_cst);
其中的RMW操作exchange是否仍能读取到值0?
不可能读取到0。
线程2会一直等待flag变为true,而flag.store(true)是在thread1执行完state.store(1, seq_cst)之后才执行的。由于所有state的操作都是seq_cst顺序,thread1的state.store(1)在全局一致性顺序中必然先于thread2的state.exchange(1)。因此thread2的exchange操作只能读取到1,而不可能是初始的0。
Q3:Load与exchange在单一全序中如何排序?
首先明确两个标准条款的含义:
[atomics.order] p3规定:某原子对象M上的原子操作A在一致性顺序上先于M上的另一原子操作B,当且仅当:
[...]
A与B并非同一原子读-改-写操作,且存在M的某原子修改X,使得A读取X存储的值,且X在M的修改顺序中先于B,或者[...]
[atomics.types.operations] p3规定:state的初始化并非原子修改:效果:用期望的值初始化对象。初始化并非原子操作([intro.multithread])。
对于原子对象的初始值0:虽然初始化不是原子修改,但该初始值会被视为修改顺序中的“起始状态”。
回到Load和exchange的排序逻辑:
- 如果thread1的
state.load(seq_cst)读取到初始值0,那么这个Load操作在一致性顺序中先于thread1后续的state.store(1),同时也先于thread2的state.exchange(1)——因为thread2的exchange读取的是thread1 store的1,而thread1的store在修改顺序中先于exchange的修改。 - 从全局seq_cst全序来看,thread1的Load、thread1的Store、thread2的Exchange会按照符合happens-before关系的顺序排列(比如
Load → Store → Exchange),此时Load先于Exchange。
简言之:Load操作如果读取的是某个修改(或初始状态)的值,且该修改在修改顺序中先于exchange操作的修改,那么Load在一致性顺序中就先于exchange。
内容的提问来源于stack exchange,提问作者xmh0511

