不,您不需要屏障,但如果读者和编写者在不同的线程中调用这些函数,您的代码无论如何都会被破坏。特别是当读者在循环中调用 read 函数时。
TL:DR:使用 C++11std::atomic<long> m_value
with return m_value++
在增量和return m_value
在读者中。这将为您提供无数据争用程序中的顺序一致性:执行将像线程以源顺序的某种交错运行一样工作。 (除非您违反规则并有其他非atomic
共享数据。)你肯定想返回一个值Increment
,如果您希望线程执行增量操作以了解它们产生的值。对于像这样的用例完全损坏后执行单独的加载int sequence_num = shared_counter++;
其中另一个线程的增量可以在之间可见count++; tmp = count;
.
如果您不需要关于操作的如此强的排序other与读取器/写入器位于同一线程中的对象,return m_value.load(std::memory_order_acquire)
足以满足大多数用途,并且m_value.fetch_add(1, std::memory_order_acq_rel)
。实际上很少有程序在任何地方都需要 StoreLoad 屏障;即使使用原子 RMW 实际上也不能重新排序acq_rel
。 (在 x86 上,它们的编译方式与您使用的相同seq_cst
.)
你不能在线程之间强制排序;负载要么看到该值,要么看不到该值,具体取决于读取线程在获取/尝试获取负载值之前或之后是否看到来自编写器的无效。线程的全部意义在于它们don't彼此步调一致地运行。
数据竞赛 UB:
循环阅读m_value
可以将负载提升出循环,因为它不是atomic
(甚至volatile
作为黑客)。这是数据竞争 UB,编译器会破坏你的阅读器。看this and 多线程程序卡在优化模式但在-O0下正常运行
障碍不是这里的问题/解决方案,只是强制重新检查内存(或当前 CPU 看到的内存的缓存一致性视图;L1d 和 L2 等实际的 CPU 缓存对此不是问题)。这并不是障碍真正的作用。他们命令该线程访问一致缓存。 C++ 线程仅跨具有一致缓存的核心运行。
但说真的,不要在没有very有说服力的理由。何时在多线程中使用 易失性?几乎从来没有。该答案解释了缓存一致性,并且您不需要障碍来避免看到过时的值。
在许多现实世界的 C++ 实现中,类似std::atomic_thread_fence()
也将是一个“编译器屏障”,强制编译器从内存中重新加载非原子变量,即使没有volatile
,但这是一个实现细节。因此,在某些 ISA 的某些编译器上,它可能运行得足够好。并且仍然不能完全安全地防止编译器发明多个加载;请参阅 LWN 文章谁害怕一个糟糕的优化编译器?有关详细信息的示例;主要针对 Linux 内核如何滚动自己的原子volatile
,事实上它是由 GCC/clang 支持的。
“最新值”
初学者经常对此感到恐慌,并认为 RMW 操作由于其指定方式而在某种程度上更好。因为它们是捆绑在一起的读+写,并且每个内存位置都有一个修改顺序分别地,RMW 操作必然必须等待对缓存行的写访问,这意味着在单个位置上串行化所有写入和 RMW。
原子变量的普通加载仍然保证(通过实际实现)立即看到值。 (ISO C++ 仅建议值should可以在有限的时间内迅速看到,但当然实际的实现可以做得更好,因为它们运行在缓存一致的 CPU 硬件上。)
两个线程之间不存在“立即”这样的东西;另一个线程中的加载看到存储的值,或者它在存储对其他线程可见之前运行但不可见。通过线程调度等,线程总是可能会加载一个值,但很长一段时间不会使用它;装载时它是新鲜的。
因此,这与正确性几乎无关,剩下的就是担心线程间延迟。在某些情况下,这可能会受到屏障的帮助(以减少后续内存操作的争用,not更快地主动清理你的商店,障碍只是等待以正常方式发生)。因此,这通常是一个非常小的影响,而不是使用额外障碍的理由。
See MESI 协议和 std::atomic - 它是否确保所有写入对其他线程立即可见?。并查看我的评论https://github.com/dotnet/runtime/issues/67330#issuecomment-1083539281 and 除了提供必要的保证之外,硬件内存屏障是否还能使原子操作的可见性更快? 常常没有,即使是这样,也不会相差太多。
当然,这不足以值得用大量额外的障碍来减慢读者的速度,只是为了让它看看这个atomic
变量晚于其他变量atomic
变量,如果您不需要该顺序来保证正确性。或者放慢作家的速度,让它坐在那里什么都不做maybe让它更快地完成 RFO 几个周期,而不是完成其他有用的工作。
如果您对线程的使用在核心间延迟方面遇到了如此严重的瓶颈,以至于这是值得的,那么这可能表明您需要重新考虑您的设计。
没有障碍或顺序,只需std::atomic
with memory_order_relaxed
,您通常仍然会在大约 40 纳秒内看到其他内核上的数据(在现代 x86 台式机/笔记本电脑上),大约与两个线程都使用原子 RMW 相同。而且它不可能被延迟任何显着的时间,比如一微秒,如果你为许多早期的存储创建了大量的争用,那么它们都需要很长时间才能在这个存储之前提交。您绝对不必担心走很长时间看不到商店。 (当然这仅适用于atomic
或手卷原子volatile
。普通的非易失性负载只能在循环开始时检查一次,然后就不再检查了。这就是为什么它们无法用于多线程。)