为什么 std::atomic<T>::is_lock_free() 不是静态的并且是常量表达式?

11

有人能告诉我,std::atomic<T>::is_lock_free() 是不是静态的,还是 constexpr 的?它如果不是静态的和/或不是 constexpr 的话对我来说就没有意义。

为什么它没有像 C++17 中的 is_always_lock_free 那样设计呢?


4
你知道is_always_lock_free吗? - Mike van Dyke
3
我会给出“对齐”这个词。 - Max Langhof
@MaxLanghof 你的意思是并不是所有的实例都会以相同的方式对齐吗? - curiousguy
1
Mike,不,我不知道,但感谢你的提示;这对我真的很有帮助。但我在想为什么要在is_lock_free()和is_always_lock_free之间做出决策。它不能是因为未对齐的原子,正如其他人在这里建议的那样,因为语言定义未对齐的访问具有未定义的行为。 - Bonita Montero
4个回答

13

如在cppreference中所解释:

所有原子类型都可以使用互斥锁或其他锁定操作进行实现,但std::atomic_flag除外。例如,在某些架构上,只有对齐的内存访问才是本质上原子的,因此同一类型的未对齐对象必须使用锁。

C++标准建议(但不要求)无锁原子操作也是地址自由的,即适用于使用共享内存进行进程间通信。

正如多个其他人所提到的,std::is_always_lock_free可能是您真正需要的。


编辑:为了澄清,C++对象类型有一个对地址进行限制的对齐值,其实例的地址仅限于2的幂次倍数([basic.align])。

例如,x86(大多数情况下)支持不对齐访问。然而,你会发现大多数编译器在x86上具有 alignof(double) == sizeof(double) == 8 ,因为不对齐访问有许多缺点(速度、缓存、原子性...)。但是例如 #pragma pack(1) struct X { char a; double b; }; 或者 alignas(1) double x; 允许您拥有"不对齐"的double。因此,当cppreference谈论"对齐的内存访问"时,它可能是以硬件类型的自然对齐方式来表述的,并没有违反其对齐要求使用C++类型的方式(这将是UB)。
这里有更多信息:什么是x86上成功的不对齐访问的实际影响? 请还要查看下@Peter Cordes的有见地的评论!

1
32位x86是一个很好的例子,其中你会发现ABIs中alignof(double)==4。但是std::atomic<double>仍然具有alignof()=8而不是在运行时检查对齐方式。使用下对齐的打包结构会破坏ABI,并且不受支持。(GCC对于32位x86更喜欢为8字节的对象提供自然对齐,但是结构打包规则会覆盖这一点,只基于alignof(T),例如在i386 System V上。G++曾经有一个错误,在结构体内部的atomic<int64_t>可能不是原子的,因为它只是假设。GCC(对于C而不是C++)仍然存在这个错误!) - Peter Cordes
2
但是,C++20 std::atomic_ref<double> 的正确实现将完全拒绝不对齐的double,或者在平台上运行时检查对齐方式,在这些平台上,普通的doubleint64_t可以小于自然对齐方式。(因为atomic_ref<T>操作的是声明为普通T的对象,并且只有最小对齐方式为alignof(T),没有机会给它额外的对齐方式。) - Peter Cordes
2
请参见 https://gcc.gnu.org/bugzilla/show_bug.cgi?id=62259 以了解已修复的libstdc++错误,以及 https://gcc.gnu.org/bugzilla/show_bug.cgi?id=65146 以了解仍然存在的C语言错误,其中包括一个纯ISO C11测试用例,该测试用例在使用当前的gcc -m32编译时显示出_Atomic int64_t的撕裂。无论如何,我的观点是真正的编译器不支持未对齐的原子操作,并且不进行运行时检查(尚未?),因此**#pragma pack__attribute__((packed))只会导致非原子性;对象仍将报告它们是“lock_free”。** - Peter Cordes
1
但是,is_lock_free() 的目的是允许实现与当前实际情况不同的工作方式;通过基于实际对齐方式的运行时检查来使用硬件支持的原子指令或使用锁。 - Peter Cordes
对我之前的评论进行更正:为了在double上使用std::atomic_ref,您需要自己确保它的对齐方式正确,例如alignas(std::atomic_ref<double>::required_alignment) double foo。否则会产生未定义行为,因此atomic_ref不需要检查对齐或处理对齐不正确的对象。https://en.cppreference.com/w/cpp/atomic/atomic_ref/required_alignment - undefined

4

您可以使用std::is_always_lock_free

is_lock_free 取决于实际的系统,无法在编译时确定。

相关解释:

原子类型有时也被允许是无锁的,例如,在某个架构上仅对齐的内存访问自然是原子性的,未对齐的相同类型对象必须使用锁。


2
std::numeric_limits<int>::max 取决于架构,但它是静态和 constexpr 的。我猜答案没有问题,但我不相信推理的第一部分。 - 463035818_is_not_a_number
1
不定义语言未对齐访问的行为,因此在运行时评估无锁性或否则是无意义的吗? - Bonita Montero
1
在语言中将后者定义为未定义行为,因此在对齐和不对齐访问之间做出决定是没有意义的。 - Bonita Montero
@BonitaMontero,C++对象对齐中存在“未对齐”和硬件喜欢的“未对齐”之分。这两者不一定相同,但在实践中它们经常是相同的。您展示的示例就是这样一个实例,其中编译器显然具有内置假设,即这两个是相同的——这仅意味着is_lock_free在该编译器上是无意义的。 - Max Langhof
1
如果存在对齐要求,那么可以非常确定原子将具有适当的对齐方式。 - Bonita Montero

1

std::atomic<T>::is_lock_free()可能会在某些实现中根据运行时条件返回truefalse

正如Peter Cordes在评论中指出的那样,运行时条件不是对齐,因为原子操作将(过度)对齐内部存储以进行高效的无锁操作,并强制不对齐会导致原子性丢失,这是未定义行为。

可能会有一种实现方式,它不会强制执行对齐,并且会基于对齐进行运行时调度,但这不是一个合理的实现方式。仅当__STDCPP_DEFAULT_NEW_ALIGNMENT__小于所需的原子对齐时,才支持pre-C++17,因为动态分配的超对齐在C++17之前不起作用。

另一个可能决定原子性的运行时条件是运行时CPU调度

在x86-64上,实现可以通过初始化期间的cpuid检测cmpxchg16b的存在,并将其用于128位原子操作,对于32位的cmpxchg8b和64位原子操作也同样适用。如果找不到相应的cmpxchg,则无法实现无锁原子性,实现将使用锁。

MSVC目前不进行运行时CPU调度。由于ABI兼容性原因,它不会对64位进行调度,由于已经不支持没有cmpxchg8b的CPU,因此也不会对32位进行调度。Boost.Atomic默认情况下不执行此操作(假设存在cmpxchg8bcmpxhg16b),但可以进行配置以进行检测。我还没有研究其他实现的情况。


1
非静态的 std::atomic<>::is_lock_free() API 允许实现中存在 alignof(std::atomic<T>) 小于 sizeof 的情况。目前的实现选择使 alignof == sizeof,因此它们不需要运行时对齐检查。这意味着在未对齐的 atomic<T> 对象上调用 is_lock_free 或任何其他成员函数是未定义的,因此返回值无关紧要。总之,这是实现的选择,而不是 ISO C++11 的约束。(虽然这是一个很好且显然的实现选择!)另外,关于运行时分派的观点也很有道理。 - Peter Cordes
1
@PeterCordes,是的,已经更正了。另一方面,我发现一个可能的原因不依赖于对齐:在C++17之前,new的对齐方式被固定为__STDCPP_DEFAULT_NEW_ALIGNMENT__,不能通过alignas增加。我不认为有些实现使用比最大无锁原子所需的更小的分配对齐方式,但这似乎是提供标准处理此问题的原因。 - Alex Guteniev
关于 new 的有趣观点。您可以考虑针对最大对象大小进行运行时对齐检查(特别是如果需要原子 RMW 仅用于读取),而不仅仅是决定它永远不会 lock_free,如果 new 对齐小于该大小。在任何主流的 x86 实现上都不是这种情况,例如我认为 MSVC 在 x86-64 上按 16 对齐(GNU/Linux 当然也是如此),并且在 32 位模式下所有内容都至少按 8 对齐。我不知道 gcc 在 AArch64 / MIPS64 / PPC64 上的 alignof(max_align_t) 是多少。我认为 AArch64 将具有 16 字节的原子基线,甚至不需要 -march 选项,但可能是 16B new。 - Peter Cordes
@PeterCordes,我们知道在许多配置中查询此内容的位置 https://godbolt.org/z/73z11c49e - Alex Guteniev

1
我已经在Windows电脑上安装了Visual Studio 2019,并且这个devenv还有一个ARMv8编译器。 ARMv8允许不对齐的访问,但比较和交换,锁定添加等必须对齐。当纯加载/纯存储使用ldpstp(32位寄存器的加载对或存储对)自然对齐时,也只能保证是原子操作。

所以我写了一个小程序来检查任意原子指针返回的is_lock_free()值。以下是代码:

#include <atomic>
#include <cstddef>

using namespace std;

bool isLockFreeAtomic( atomic<uint64_t> *a64 )
{
    return a64->is_lock_free();
}

这是isLockFreeAtomic的反汇编结果。
|?isLockFreeAtomic@@YA_NPAU?$atomic@_K@std@@@Z| PROC
    movs        r0,#1
    bx          lr
ENDP

这只是返回true,也就是1

这个实现选择使用alignof( atomic<int64_t> ) == 8,因此每个atomic<int64_t>都被正确对齐。这避免了在每次加载和存储时需要运行时对齐检查的需要。

(编辑者注:这是很常见的;大多数实际的C++实现都是这样工作的。这就是为什么std::is_always_lock_free非常有用的原因,因为它通常适用于is_lock_free()为真的类型。)


1
是的,大多数实现选择给出atomic<uint64_t>alignof() == 8,这样他们就不必在运行时检查对齐方式。这个旧的API给了他们不这样做的选项,但在当前的硬件上,要求对齐更有意义(否则UB,例如非原子性)。即使在32位代码中,int64_t可能只有4字节对齐,atomic<int64_t>也需要8字节。请参见我在另一个答案中的评论 - Peter Cordes
如果编译器选择使基本类型的alignof值与硬件的“良好”对齐方式相同,那么is_lock_free将始终为trueis_always_lock_free也是如此)。你的编译器正是这样做的。但是API存在的目的是让其他编译器可以执行不同的操作。 - Max Langhof
1
你可以非常确定,如果编程语言规定未对齐访问具有未定义的行为,那么所有原子操作都必须被正确地对齐。由于这个原因,没有任何实现会进行运行时检查。 - Bonita Montero
1
@BonitaMontero 是的,但是语言中没有任何禁止 alignof(std::atomic<double>) == 1 的内容(因此在 C++ 中不会有“未对齐访问”,因此也不会有 UB),即使硬件只能保证在 4 或 8 字节边界上进行无锁原子操作。编译器将在未对齐的情况下使用锁定(并根据对象实例的内存位置返回适当的布尔值从 is_lock_free 中)。 - Max Langhof
1
@MaxLanghof:是的,非静态的std::atomic<>::is_lock_free() API被设计成允许这种实现选择。但在实际的应用中,这将是一个糟糕的选择。调用它时,如果std::atomic<>对象的对齐方式小于其alignof,已经属于未定义行为,因此它仍然返回true并不违反任何规定,只是意味着该API无法检测到该问题。 - Peter Cordes

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接