在 C++ 中与 "versioning" 同步

Synchronization with "versioning" in c++

请考虑以下同步问题:

initially:
    version = 0           // atomic variable
    data = 0              // normal variable (there could be many)

Thread A:
    version++
    data = 3

Thread B:
    d = data
    v = version
    assert(d != 3 || v == 1)

基本上,如果线程 B 看到 data = 3,那么它也必须看到 version++

为了始终满足线程 B 中的断言,我们必须强加的最弱内存顺序和同步是什么?

如果我对 C++ memory_order 的理解正确,则不会执行发布-获取顺序,因为这保证了线程 A 中 version++ 之前的操作将被 [=15 之后的操作看到=], 在线程 B.

获取和释放围栏的作用方向相同,但更为通用。

正如我所说,我需要另一个方向:B看到data = 3 暗示 B看到version = 1.

我正在使用这种“版本控制方法”来尽可能避免在我正在设计的数据结构中出现锁定。当我看到有什么变化时,我退后一步,阅读新的 version 并重试。

我正在努力使我的代码尽可能可移植,但我的目标是 x86-64 CPU。

您可能正在寻找 SeqLock, as long as your data doesn't include pointers. (If it does, then you might need something more like RCU 来保护可能加载指针、暂停/休眠一段时间,然后在很久以后取消引用该指针的读者。)

您可以使用SeqLock序列计数器作为版本号。 (version = tmp_counter >> 1 因为每次写入有效负载需要 两个 增量,让读者在读取 non-atomic data 时检测撕裂。并确保他们查看与此序列号一起使用的 data。确保您没有第 3 次读取原子计数器;使用您读入的本地 tmp 来验证匹配 before/after 复制 data.)

如果读者在 data 被修改时碰巧尝试阅读,他们将不得不重试。但它是 non-atomic,所以如果线程 B 看到 data = 3 就不可能 可以成为创建同步的一部分;它只能是你看到的东西作为与作者的版本号同步的结果

参见:

  • - 我在 C++ 中对 SeqLock 的尝试,有很多评论。这有点 hack,因为 ISO C++ 的 data-race UB 规则过于严格; SeqLock 依赖于检测可能的撕裂而不是使用撕裂的数据,而不是完全避免并发访问。这在没有硬件竞争检测的机器上很好,所以不会出错(就像所有真正的 CPU 一样),但 C++ 仍然调用该 UB,即使 volatile(尽管这使它更多地进入 implementation-defined 领域) .在实践中它很好。

  • GCC reordering up across load with `memory_order_seq_cst`. Is this allowed? - 8.1 中修复的 GCC 错误可能会破坏 seqlock 实现。

如果你有多个写者,你可以使用sequence-counter本身作为写者之间互斥的自旋锁。例如使用 atomic_fetch_or 或 CAS 尝试设置低位以使计数器变为奇数。 (tmp = seq.fetch_or(1, std::memory_order_acq_rel);,希望编译到 x86 lock bts)。如果它之前没有设置低位,则该作者赢得了比赛,但如果是,则您必须重试。

但是只有一个写入器,您不需要对原子序列计数器进行 RMW,只需存储新值(通过写入负载排序),因此您可以保留它的本地副本,或者只保留它轻松加载它,并存储 tmp+1tmp+2.