我有以下代码:
#include <atomic>
int main () {
std::atomic<uint32_t> value(0);
value.fetch_add(1, std::memory_order::relaxed);
static_assert(std::atomic<uint32_t>::is_always_lock_free);
return 0;
}
它编译,所以这意味着std::atomic<uint32_t>::is_always_lock_free
是真的。
然后,gcc 10 和-std=c++20 -O3 -mtune=skylake-avx512 -march=skylake-avx512
的汇编代码如下所示:
0000000000401050 <main>:
401050: c7 44 24 fc 00 00 00 mov DWORD PTR [rsp-0x4],0x0
401057: 00
401058: f0 ff 44 24 fc lock inc DWORD PTR [rsp-0x4]
40105d: 31 c0 xor eax,eax
40105f: c3 ret
许多帖子指出,读取-修改-写入操作(fetch_add()
这里)不能是没有锁的原子操作。
我的问题是std::atomic::is_always_lock_free
成为true
的真正含义。
本页声明Equals true if this atomic type is always lock-free and false if it is never or sometimes lock-free.
那么"这种原子类型总是无锁的"是什么意思呢?
这里的"Lock"是"互斥"的意思,而不是专门指名为lock
的x86指令前缀。
为任意类型实现std::atomic<T>
的一种简单而通用的方法T
是作为一个包含T
成员和std::mutex
的类,该围绕对象的每个操作(加载,存储,交换,fetch_add等)被锁定和解锁。 然后,这些操作可以用任何旧方式完成,并且不需要使用原子机器指令,因为锁可以保护它们。 此实现不会无锁。
这种实现的一个缺点是,除了通常很慢之外,如果两个线程尝试同时对对象进行操作,其中一个线程将不得不等待锁定,这实际上可能会阻塞并导致它被调度一段时间。 或者,如果一个线程在保持锁的同时被调度出来,则每个想要对该对象进行操作的线程都必须等待第一个线程被调度回来并首先完成其工作。
因此,如果机器支持真正的原子操作,T
是可取的:其他线程无法干扰的单个指令或序列,并且在中断(或者可能根本无法中断)时不会阻塞其他线程。 如果对于某种类型的T
,库已经能够专门使用这样的实现来std::atomic<T>
,那么这就是我们所说的无锁的意思。 (在 x86 上只是令人困惑,因为用于此类实现的原子指令被命名为lock
。 在其他架构上,它们可能被称为其他东西,例如 ARM64 的ldxr/stxr
专用加载/存储指令。
C++标准允许类型"有时无锁":也许在编译时不知道std::atomic<T>
是否会无锁,因为它取决于将在运行时检测到的特殊机器功能。 甚至有可能某些std::atomic<T>
类型的对象是无锁的,而其他对象则不是。 这就是为什么atomic_is_lock_free
是一个函数而不是一个常量。 它检查此特定对象在这一特定日期是否无锁定。
但是,对于某些实现,可能的情况是,在编译时可以保证某些类型始终是无锁的。 这就是is_always_lock_free
用来指示的内容,请注意,它是一个constexpr bool
而不是一个函数。