【问题标题】:Why are loads required when using atomics为什么使用原子时需要加载
【发布时间】:2021-07-08 23:14:12
【问题描述】:

在 Go(和其他语言,如 c++)中使用原子时,建议使用原子加载操作来读取并发写入的值。

如果原子写入(无论是存储还是整数增量)的定义(据我所知)是没有线程可以查看部分写入,为什么需要原子加载?

如果仅在该内存地址上使用原子存储,那么该内存地址的普通加载是否总是安全的?

【问题讨论】:

    标签: multithreading concurrency atomic


    【解决方案1】:

    这个答案主要针对 C 和 C++,因为我并不直接熟悉许多其他语言中的原子,但我怀疑它们是相似的。

    在某些情况下,许多实际机器确实以这种方式工作。例如,在 x86-64 上,普通加载指令相对于普通存储或锁定的读-修改-写指令是原子的。所以对于可以通过单条指令加载的类型,原则上可以使用普通赋值避免撕裂。

    但在某些情况下这不起作用。例如:

    • 非无锁类型(例如,多于几个单词的结构)。在这种情况下,需要加载或存储几条指令,因此必须在它们周围加锁,否则完全有可能撕裂。 atomic load 函数知道获取锁,普通赋值不会。

    • 可以无锁但需要特殊处理的类型。例如,x86-32 上的 64 位 long long int。一个普通的加载将执行两个 32 位整数加载指令(它们是单独的原子的),因此即使存储是原子的,它也可能发生在两者之间。但是atomic 加载函数可以发出 64 位浮点或 SIMD 加载,效率较低,但可以在一条原子指令中执行。 Example on godbolt.

    因此,该语言在存储和加载使用提供的atomic 函数时承诺原子性。 - 您的“定义”对于 C 或 C++ 不准确。通过要求程序员始终使用atomic 加载,该语言提供了一个“挂钩”,实现可以在需要时采取适当的行动。在普通负载就足够的情况下,实现可以相应地优化而不会丢失任何内容。

    另一点是atomic 加载提供了一个放置内存屏障的地方,当需要时(除relaxed 之外的任何排序)。一些架构包括带有内置屏障的加载指令(例如 ARM64 的 ldar),并且在语言级别将屏障作为负载的一部分使编译器更容易利用这一点。如果您必须在调用屏障函数之后进行常规赋值,编译器将更难确定是否可以将它们优化为ldar

    【讨论】:

    • 感谢您的回答。总而言之,原子包充当底层机制的抽象。原子包的用户所要保证的是,只要您使用加载方法,就没有线程可以读取一半写入。恰巧像 x86 这样的架构允许对字长进行原子操作,这些操作并不严格要求使用 atomic.Load(使用整数时)——但这并不安全,因为您的代码可以为其他架构编译。这听起来对吗?
    • @LouisDelo:是的,没错。
    • 感谢您帮助我解决这个问题。赞赏。
    猜你喜欢
    • 1970-01-01
    • 2021-05-21
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2018-08-26
    • 1970-01-01
    • 2019-11-13
    相关资源
    最近更新 更多