|
| 1 | +--- |
| 2 | +tags: |
| 3 | + - C++ |
| 4 | + - 并发编程 |
| 5 | + - 同步机制 |
| 6 | +--- |
| 7 | + |
| 8 | +# 关于原子操作重新思考 |
| 9 | + |
| 10 | +首先对于单个原子操作语句来说, 它在外部看来一定是原子操作(废话), 也就是不存在“中间态”. |
| 11 | + |
| 12 | +## 操作 |
| 13 | + |
| 14 | +先重新看待三种操作(先看操作本身, 再考虑 memory order) —— Load, Store, RMW. |
| 15 | + |
| 16 | +Load 对应我们完全不需要考虑什么, 只是单纯的读取; Store 是我们不需要在意旧的值是多少, 只管写入即可; 而 RMW 是我们在需要旧值的前提下(比如迭代数据 / 条件判断等) 在进行修改数据 |
| 17 | + |
| 18 | +小心谨慎, 永远不要假定代码顺序就是执行顺序, 代码下一行的状态和上一行状态一致. |
| 19 | + |
| 20 | +```C++ |
| 21 | +if (state.load() == State::Idle) |
| 22 | +{ |
| 23 | + state.store(State::Loading); |
| 24 | +} |
| 25 | +``` |
| 26 | + |
| 27 | +借助“打脸法”, 我们不妨假设 `store` 的时候 `state` 的状态已经不是 `Idle` 了, 那么这符合我们的业务逻辑吗? 如果不符合说明写的不对. 又因为此时我们是需要知道旧值的前提下进行数据修改, 所以可以比较自然的想到使用 RMW 操作. 那么正确的写法水到渠成 —— |
| 28 | + |
| 29 | +``` |
| 30 | +auto expected = State::Idle; |
| 31 | +state.compare_exchange(expected, State::Loading); |
| 32 | +``` |
| 33 | + |
| 34 | +如果正着想“我们需要利用旧值和写入整个是一个原子操作, 中途不允许其他线程之类的介入”, 总觉得跳过了什么思考过程, 所以不妨“打脸”, 举反例证明代码的业务正确性 ( 其实只是为了掩饰我对于这种思考模型的不熟练罢了 ). |
| 35 | + |
| 36 | +害, 大抵还是证伪比证明正确更简单罢. |
| 37 | + |
| 38 | +## Memory Order |
| 39 | + |
| 40 | +有了上述理解之后, 我们再看 Memory Order, 这个更需要把业务逻辑代入进行思考. |
| 41 | + |
| 42 | +首先还是 relaxd —— 只关心当前操作本身, 不需要在意其他事情, 所以最简单 |
| 43 | + |
| 44 | +然后是 release, 它不单纯是说明了一个数据的存储, 更是说明我需要借助这个原子操作发布“一些数据”(比如借助sequenced-before来“保护”单线程内前面的代码执行等), 所以这个“发布”的感觉其实和生产者的职能比较符合 |
| 45 | + |
| 46 | +那么作为一对的 acquire, 说明的就是我现在需要进行一个“整理”, 接收之前可能会遇到的 release 所发布的数据并且进行消费. 可以说和消费者的语义比较吻合. |
| 47 | + |
| 48 | +那么还有一个“中间件” —— acq_rel, 表示消费了前面的数据, 同时写入的新状态作为下一个发布 |
| 49 | + |
| 50 | +但是这样说, 在写代码的时候依旧难以去思考, 所以还是沿用打脸 —— 我写下这个代码, 假设就是 relaxed, 那么业务是否会成立? |
| 51 | + |
| 52 | +```C++ |
| 53 | +// 生产者 |
| 54 | +data = "Hello"; // A |
| 55 | + |
| 56 | +ready.store( // B |
| 57 | + true, |
| 58 | + std::memory_order_relaxed); |
| 59 | + |
| 60 | +// 消费者 |
| 61 | +if (ready.load(std::memory_order_relaxed) == true) // C |
| 62 | +{ |
| 63 | + Use(data); // D |
| 64 | +} |
| 65 | +``` |
| 66 | + |
| 67 | +我们看消费者, 我现在读取到了 ready == true, 那么在没有任何保护的情况下, 可以保证 data 一定被写入吗? 不行, 在C++内存模型中不能保证这件事情, 因为 relaxed 无法建立跨线程的同步. 所以打脸, 不能用 relaxed. |
| 68 | + |
| 69 | +不过此处或许也可以看成一个同步桥? —— 我们知道了 `A -> B` 和 `C -> D`, 但是目前使用 relaxed 或许只能建立起 `C` 是读取 `B` 中的数据, 但是并无法建模出一条完整的同步链 `A -> B -> C -> D` 也就是两个线程数据间的桥梁没有建立起来. |
| 70 | + |
| 71 | +所以看业务代码, 有没有要求要搭桥, 要的话就跟据桥的拓扑结构进行 acq 和 rel 的各种选择 |
| 72 | + |
| 73 | +等等, 我又想到一种解释 —— 可以看当前的原子语句执行的时候, 除了对于自身的数据的读写等之外, 是否还有承担其他的语义, 比如借助这个操作来保护一些变量等, 如果有, 那么就需要重新考虑 acq/rel 等, 如果没有则可以是 relaxed. |
0 commit comments