UncP/aili

mass_node_get_version 只使用了 acquire, 是否存在风险?

Closed this issue · 8 comments

根据专栏文章 https://zhuanlan.zhihu.com/p/52624601 提供的伪代码,

uint32_t before = node_get_stable_version(n);
// read node here
uint32_t after = node_get_version(n); // no need to be stable, just latest version
if ((before ^ after == LOCK_BIT) || (before ^ after == 0))
    // neither insert nor split happened

acquire 保证之后的读操作不会重排至当前操作之前, 但不保证之前的读操作不重排至当前操作之后, 换言之, after 的值可能比 read node 过程中的值从全局时间线来看更早.

实际运行有没有可能是这样的?

uint32_t before = node_get_stable_version(n);
uint32_t after = node_get_version(n); // no need to be stable, just latest version
// read node here
// WE HAVE A BIG PROBLEM NOW
if ((before ^ after == LOCK_BIT) || (before ^ after == 0))
    // neither insert nor split happened

是不是用 memory_order_seq_cst 更安全一点?

X86 保证 load load 不乱序, 只要编译器不重排是没有问题的.

https://godbolt.org/z/PdnlZQ

实际看起来 GCC 还是相当保守的, 对于 load 操作似乎无论什么 order 都生成统一的汇编. memory_order_seq_cst 也只是相对多了一个 total order 并没有说不允许 load 重排到另一个 load 之后, 疑惑...

目前我的解释是可观测的 load load 重排根本不存在, 如果存在的话, 几乎无法写任何代码. 参见 https://stackoverflow.com/questions/26190364/is-it-legal-for-a-c-optimizer-to-reorder-calls-to-clock

我倾向于认为这个是标准没有明确, 但现实实现不可能出现的问题. 如果我错了还请麻烦告知~

感谢回复, 我同意 atomic 自带 barrier, 但是 6 种 memory order 哪一种是限制 load load reorder 的呢?

UncP commented

编译器是不会改变代码语义的,所以 a(a.1, a.2, a.3) 在代码中是在 b 前面,那么不管怎么 reorder 都不会改变你写的代码的语义,即使 a.3 排在了 b 后面执行,结果和 (a.1, a.2, a.3 b) 是一样的。

这里 acquire 的语义是保证读到 version 的最新值;但是这行代码所在的位置是保证读 version 这个操作在 read node 结束后执行,所以你说的第二种特殊情况时不会发生的。

我基本清晰了。我个人的解释是 get version 不是去拿函数内 stack 上的 local variable,而是通过指针从 node 上取值,这存在潜在的"observable side effects"。编译器天然就不允许这种 reorder。限制这种 reorder 的并不是 memory order,而是你说的语义或者上面 SO 提到的"observable side effects"。感谢~