代码之家  ›  专栏  ›  技术社区  ›  Kraken

原子如何确保原子性

  •  4
  • Kraken  · 技术社区  · 7 年前

    如果我有密码 a = a + 1 ,现在我了解执行此操作需要多个CPU级别的操作,但是如何定义 a 作为 std::atomic<int> 使这些多事务成为原子事务?

    它是否改变了CPU指令的执行方式?我假设它必须以某种方式将指令的数量缩小到1,这样任何上下文切换都不会导致不可靠的结果,但是它是如何做到的呢?

    如果编译器总是可以创建这样的代码,为什么不总是这样做呢?

    2 回复  |  直到 7 年前
        1
  •  6
  •   Matthieu Brucher    7 年前

    如果有可以发出的原子指令(对于已知的可能的原子操作),那么就发出这个原子指令,否则它将具有锁机制。

    有一个函数(C++ 17)告诉你原子类型是否始终是无锁的: is_always_lock_free . 请注意,如果此函数返回 false 至少有些操作不是无锁的(不一定全部是无锁的)。这些非无锁操作通常比原子操作更昂贵(它们本身比传统操作更昂贵)。

    并非所有硬件都支持原子操作的所有组合,因此不同的编译器后端将生成不同的解决方案,有时使用单个原子操作,有时使用锁定机制。

    所以它不能总是创建这样的单指令代码。

        2
  •  3
  •   BeeOnRope    7 年前

    [B]但是将a定义为std::atomic是如何使这些倍数 事务原子?

    它不会使“多个事务”在任意表达式中成为原子(例如,它在 a = a + 1 例子)。相反,你需要像 a++ 它保证是原子的。在这种情况下,如何实现它取决于编译器和硬件,但最常见的策略是:

    • 一条指令 原子操作 它可以自动增加内存中的值。在x86上,这是 something like 这个 lock add 指令。
    • 某种类型的 compare-and-exchange (CAS) load-linked store-conditional (LL-SC) 循环用于以原子方式重复尝试执行增量。你可以 see LL-SC in action 在MIPS64上。
    • 最后,在不支持此类操作的平台上,或者在数据类型与这些指令不兼容的平台上,可以获得一个锁,以在使用常规的非原子指令执行操作时排除任何并发访问。对于原子类型,大多数主流平台不需要依赖于此,但是您仍然可以看到 an example here 在旧的ARM编译器上。

    您可以通过检查生成的程序集来检查编译器和硬件组合的行为。有时这很棘手,因为编译器可能调用在运行时库中实现的函数,在这种情况下,您必须检查该函数的源代码或反汇编。这意味着,如果运行库实现不同,相同的二进制文件可以在不同的主机上具有不同的原子操作实现!

    如果编译器总是可以创建这样的代码,为什么不总是这样做呢? 那?

    编译器并不总是生成这些,因为它们在硬件级别上很昂贵。例如,正常(非原子)添加通常需要1个周期或更少 在大多数现代CPU上 ,而原子加成可能需要15到100个周期。使用cas或ll-sc的方法通常更慢,需要重试循环,这会增加二进制文件的大小。


    在一些微控制器类CPU上可能有几个周期,但是原子操作通常不那么重要,因为可能没有多个核心。

    这取决于你如何测量它-一个加法通常需要一个周期 完成 (延迟),但您通常可以在同一个周期中执行多个独立的加法。例如,现代Intel CPU可以在一个周期内执行四个。

    推荐文章