Skip to content

关于原子操作重新思考 ​

首先对于单个原子操作语句来说, 它在外部看来一定是原子操作(废话), 也就是不存在“中间态”.

操作 ​

先重新看待三种操作(先看操作本身, 再考虑 memory order) —— Load, Store, RMW.

Load 对应我们完全不需要考虑什么, 只是单纯的读取; Store 是我们不需要在意旧的值是多少, 只管写入即可; 而 RMW 是我们在需要旧值的前提下(比如迭代数据 / 条件判断等) 在进行修改数据

小心谨慎, 永远不要假定代码顺序就是执行顺序, 代码下一行的状态和上一行状态一致.

C++
if (state.load() == State::Idle)
{
    state.store(State::Loading);
}

借助“打脸法”, 我们不妨假设 store 的时候 state 的状态已经不是 Idle 了, 那么这符合我们的业务逻辑吗? 如果不符合说明写的不对. 又因为此时我们是需要知道旧值的前提下进行数据修改, 所以可以比较自然的想到使用 RMW 操作. 那么正确的写法水到渠成 ——

auto expected = State::Idle;
state.compare_exchange(expected, State::Loading);

如果正着想“我们需要利用旧值和写入整个是一个原子操作, 中途不允许其他线程之类的介入”, 总觉得跳过了什么思考过程, 所以不妨“打脸”, 举反例证明代码的业务正确性 ( 其实只是为了掩饰我对于这种思考模型的不熟练罢了 ).

害, 大抵还是证伪比证明正确更简单罢.

Memory Order ​

有了上述理解之后, 我们再看 Memory Order, 这个更需要把业务逻辑代入进行思考.

首先还是 relaxd —— 只关心当前操作本身, 不需要在意其他事情, 所以最简单

然后是 release, 它不单纯是说明了一个数据的存储, 更是说明我需要借助这个原子操作发布“一些数据”(比如借助sequenced-before来“保护”单线程内前面的代码执行等), 所以这个“发布”的感觉其实和生产者的职能比较符合

那么作为一对的 acquire, 说明的就是我现在需要进行一个“整理”, 接收之前可能会遇到的 release 所发布的数据并且进行消费. 可以说和消费者的语义比较吻合.

那么还有一个“中间件” —— acq_rel, 表示消费了前面的数据, 同时写入的新状态作为下一个发布

但是这样说, 在写代码的时候依旧难以去思考, 所以还是沿用打脸 —— 我写下这个代码, 假设就是 relaxed, 那么业务是否会成立?

C++
// 生产者
data = "Hello";                                         // A

ready.store(                                            // B
    true,
    std::memory_order_relaxed);

// 消费者
if (ready.load(std::memory_order_relaxed) == true)      // C
{
    Use(data);                                          // D
}

我们看消费者, 我现在读取到了 ready == true, 那么在没有任何保护的情况下, 可以保证 data 一定被写入吗? 不行, 在C++内存模型中不能保证这件事情, 因为 relaxed 无法建立跨线程的同步. 所以打脸, 不能用 relaxed.

不过此处或许也可以看成一个同步桥? —— 我们知道了 A -> B 和 C -> D, 但是目前使用 relaxed 或许只能建立起 C 是读取 B 中的数据, 但是并无法建模出一条完整的同步链 A -> B -> C -> D 也就是两个线程数据间的桥梁没有建立起来.

所以看业务代码, 有没有要求要搭桥, 要的话就跟据桥的拓扑结构进行 acq 和 rel 的各种选择

等等, 我又想到一种解释 —— 可以看当前的原子语句执行的时候, 除了对于自身的数据的读写等之外, 是否还有承担其他的语义, 比如借助这个操作来保护一些变量等, 如果有, 那么就需要重新考虑 acq/rel 等, 如果没有则可以是 relaxed.

Released under the MIT License.