atomic_flag是如何实现的?

How is atomic_flag implemented?

atomic_flag是如何实现的?我觉得在 x86-64 上它相当于 atomic_bool 无论如何,但这只是一个猜测。 x86-64 实现可能与 arm 或 x86 有什么不同吗?

是的,在 atomic<bool>atomic<int> 也是无锁的普通 CPU 上,它与 atomic<bool> 非常相似,使用相同的指令。 (x86 和 x86-64 具有相同的可用原子操作集。)

您可能认为它总是使用 x86 lock btslock btr 来设置/重置(清除)单个位,但做其他事情可能更有效(尤其是对于returns 一个 bool 而不是在其上分支的函数)。该对象是一个完整的字节,因此您可以只存储或交换整个字节。 (如果 ABI 保证该值始终为 01,则您不必在将结果作为 bool 返回之前对其进行布尔化)

GCC 和 clang 将 test_and_set 编译为字节交换,并清除为 0 的字节存储。 我们得到(几乎)相同的 asm atomic_flag test_and_set 作为 f.exchange(true);

#include <atomic>

bool TAS(std::atomic_flag &f) {
    return f.test_and_set();
}

bool TAS_bool(std::atomic<bool> &f) {
    return f.exchange(true);
}


void clear(std::atomic_flag &f) {
    //f = 0; // deleted
    f.clear();
}

void clear_relaxed(std::atomic_flag &f) {
    f.clear(std::memory_order_relaxed);
}

void bool_clear(std::atomic<bool> &f) {
    f = false; // deleted
}

On Godbolt 用于带有 gcc 和 clang 的 x86-64,以及用于 ARMv7 和 AArch64。

## GCC9.2 -O3 for x86-64
TAS(std::atomic_flag&):
        mov     eax, 1
        xchg    al, BYTE PTR [rdi]
        ret
TAS_bool(std::atomic<bool>&):
        mov     eax, 1
        xchg    al, BYTE PTR [rdi]
        test    al, al
        setne   al                      # missed optimization, doesn't need to booleanize to 0/1
        ret
clear(std::atomic_flag&):
        mov     BYTE PTR [rdi], 0
        mfence                          # memory fence to drain store buffer before future loads
        ret
clear_relaxed(std::atomic_flag&):
        mov     BYTE PTR [rdi], 0      # x86 stores are already mo_release, no barrier
        ret
bool_clear(std::atomic<bool>&):
        mov     BYTE PTR [rdi], 0
        mfence
        ret

请注意,xchg 也是一种在 x86-64 上执行 seq_cst 存储的有效方法,通常比 gcc 使用的 mov + mfence 更有效. Clang 对所有这些都使用 xchg(relaxed store 除外)。

有趣的是,clang 在 atomic_flag.test_and_set() 中的 xchg 之后重新布尔化为 0/1,但 GCC 在 atomic<bool> 之后执行它。 clang 在 TAS_bool 中做了一个奇怪的 and al,1,这会将像 2 这样的值视为 false。这似乎毫无意义; ABI 保证内存中的 bool 总是存储为 01 字节。

对于 ARM,我们有 ldrexb / strexb 交换重试循环,或者只有 strb + dmb ish 用于纯存储。或者 AArch64 可以将 stlrb wzr, [x0] 用于 clear 或 assign-false 来执行(零寄存器的)顺序释放存储,而无需屏障。

在 most/sane 架构上,中断可能发生在执行硬件指令之后或之前。不是 "in between" 这是执行。所以指令 "happens"(即 "side effects")或者不发生。

例如,一个 16 位架构很可能具有硬件指令,可以通过一条指令对 16 位变量进行操作。因此递增 16 位变量将是一条指令。在 16 位变量中存储一个值将是一条指令。等等。 16 位变量不需要锁定,因为增量要么自动发生,要么不发生。在这种体系结构上不可能观察到 16 位变量增量的 "mid execution" 状态。这是一条指令。它不能被任何信号和中断打断"in between"。

16 位体系结构可能缺少在单个指令中递增 64 位变量的指令。它可能需要很多很多指令来对 64 位变量进行操作。所以对 std::atomic<uint64_t> 的操作需要编译器插入额外的同步指令来实现它的功能,实现与其他 std::atomic 变量的同步,等等

但是在这个架构上对 16 位变量的操作是单条指令,编译器不需要对它们做任何事情,指令执行后副作用将始终随处可见。

所以 atomic_flag 很可能只是一个变量,它具有特定处理器上的字的大小。这是为了使该处理器可以使用单个指令对该变量进行操作。实际上,这是一个 int,但 int 不能保证对应于处理器的字长,并且访问 int 句柄不能保证是原子的。我相信通常 atomic_flagsig_atomic_t from posix (posix docs 相同)。额外的 atomic_flag 限制它的操作仅 bool-ish 像:清除、设置和通知。