【问题标题】:C++ std::memory_order_relaxed and skip/stop flagC++ std::memory_order_relaxed 和跳过/停止标志
【发布时间】:2017-10-11 06:00:14
【问题描述】:

是否可以使用std::memory_order_relaxed 作为跳过标志,如iterate

    constexpr static const std::size_t capacity = 128; 
    std::atomic<bool> aliveness[capacity];
    T data[capacity];     // immutable/atomic/etc.

    template<class Closure>
    void iterate(Closure&& closure){
        for(std::size_t i = 0; i<capacity; i++){
            if (!aliveness[i].load(std::memory_order_relaxed)) continue;                    
            closure( data[i] );
        }
    }

    void erase(std::size_t index){
        aliveness[index].store(false, std::memory_order_relaxed);
    }

或者我应该使用 release/acquire 来代替?

aliveness[i] 可能会再次“活着”。

iterateerase 同时从多个线程调用。考虑data immutable/atomic/synchronized 在其他一些外部锁下。

【问题讨论】:

  • 你说“可以吗”是什么意思?
  • 为什么size 是原子的,为什么你要在for 循环的每个循环迭代中重新读取它(使用默认的 seq_cst 加载)?
  • @PeterCordes 更新为更简单的情况。
  • 通过询问“是否可以”,我的意思是 - 如果在一个线程中执行erase,那么另一个线程是否iterate 会看到该标志变为假;以及多久。是否有可能我们在一个线程中调用erase,但另一个线程没有看到该标志发生变化?

标签: c++ multithreading stdatomic memory-barriers


【解决方案1】:

假设:当您使用erase 时,其他线程可以随时运行iterate()。 (该问题的早期版本没有指定不可变。如果更新data[i] 的锁定(或缺少锁定)未按顺序写入alive[i],则此答案仍然相关。)

如果数据确实是不可变的,那么mo_relaxed 绝对没问题,除非您需要根据线程正在执行的操作else 对这些商店的全局可见性进行排序。 mo_relaxed 存储最终将始终对其他线程可见(在当前的 CPU 上,这样做会非常快)。


如果您要修改非原子data[i]alive[i] 为假,您需要确保其他线程在修改它时没有使用它的值。那将是 C++ 中的 UB,而实际硬件上的实际正确性问题取决于 Tclosure

获取语义适用于iterate。对data[i] 的访问逻辑上发生在alive[i] 之后,因为单向障碍的方向正确。 The acquire-load won't reorder with later loads or stores, only earlier ones.

但是erase 的商店有问题。在对data[i] 进行任何修改之前,它必须是全局可见的。但是发布商店可以与后来的商店重新订购。你需要的是a release fence to block reordering of stores in both directions

void erase(std::size_t index){
    aliveness[index].store(false, std::memory_order_relaxed);
    std::atomic_thread_fence(std::memory_order_release);
  // or don't do that, this probably wouldn't be enough synchronization
  // and whatever you do that syncs the data might make this unnecessary.
}

如果T 是原子类型,则data[i] 的发布存储就可以解决问题。但不要那样做;如果T 太大而不能成为无锁原子,那就太糟糕了。

(更新:我不确定这是否完全有效。ISO C++ 内存排序规则是根据 synchronizes-with/happens-before 定义的,以及允许给定负载的值看。不是在原子操作和非原子操作之间重新排序本身。

此外,这就像您对 SeqLock 所做的那样,但这也取决于数据争用 UB 进行读取。如果另一个线程要读取data[i],如果它检查alive[i] 然后读取,则存在TOCTOU 竞赛:我们对alive 的写入可能发生在读取之后,然后我们对data[i] 的写入可能与另一个线程同时发生线程阅读。 所以这可能不足以擦除/修改/取消擦除元素,即使在像 x86 这样的强排序非怪异机器上也是如此。)


在某些实现中,seq-cst 存储也可以工作,但我认为仅作为实现细节。它通常会导致存储 + 全屏障 asm 指令。 (例如 x86 MFENCE)。所以它之所以有效,只是因为编译器将 seq-cst 存储实现为 store + thread_fence(seq_cst)。在 AArch64 上情况并非如此,因为 STLR 只是一个释放操作;只有与 LDAR 的交互才能产生 SC-DRF。 (seq_cst 用于无数据竞争的程序)。


请注意,如果closure 修改data[]iterate 是不安全的,除非一次只有一个线程可以调用它。在这种情况下,这有什么意义?所以可能你应该使用

void iterate(Closure&& closure) const
{ ... }

所以iterate 仅适用于容器的const 对象。

【讨论】:

  • 考虑data不可变/原子。对于这种不准确之处,我们深表歉意。问题是关于aliveness 线程之间的可见性/传播(现在才开始理解这一点)。对它的更改是否对所有线程立即可见,或者它可以用以前的值误读。比如:我们在 Thread1 中将其设置为 false,但 Thread2 读取了之前的 true 值。
  • @tower:存储到aliveness[i] 最终将对所有其他线程中的负载可见。 (通常非常快,就像生产者 CPU 中存储缓冲区的长度 + 消费者 CPU 中早期读取的乱序窗口的大小)。但是,是的,负载的无序执行会导致不同的消费者看到不同的值。由于无序执行,您必须小心如何定义“同时”。如果这些线程之间没有某种其他方式进行交互,那么这是一个毫无意义的问题,因为它们之间没有同步。
  • 所以,在原样示例中 - 这是相当机会主义/投机性的擦除:),但如果我切换到获取 - 释放语义,它将不会出现错误读数?
  • @tower120:不,如果您不修改data[i],那么将其称为“假”是没有意义的。线程之间根本没有排序。让iterate 使用在相应的alive 值之前加载的data 值并没有“错误”,因为C++ 标准确实鼓励实现使轻松存储对其他线程快速可见。您可以假设它不会很快出现。它可能会使iterate 运行速度变慢,以确保它不使用提前加载的data 值。
  • 我猜,你是对的,让迭代一两个额外的元素......
猜你喜欢
  • 2021-01-26
  • 1970-01-01
  • 1970-01-01
  • 2011-04-25
  • 2014-03-12
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2018-05-08
相关资源
最近更新 更多