atomic_flag_test_and_set
是的!atomic_flag_clear
是的!atomic_flag_test_and_clear
没有atomic_flag_set
没有
如果您想在某些上下文中对事件设置标志,并在其他上下文中检查并清除事件,C/C++ 不允许您在每个上下文中执行单个原子调用。
您必须反转标志,因此清除事件上的标志,在检查事件时检查并设置标志。
没什么大不了的,但在这种情况下似乎是倒退的,特别是考虑到标志的默认状态为 false,这在相反的意义上意味着默认情况下会断言事件。
我想,也可以使用原子bool
with来代替。atomic_exchange
实用建议:如果有限的操作集使您的代码效率较低,请在正常代码中使用
atomic<bool>
或atomic<unsigned char>
代替。atomic_flag
除非您关心在非常原始的机器上实现无锁,atomic<bool>
并且atomic<int>
可能不是无锁的。(或者也许使用 C++20std::atomic_unsigned_lock_free
。)TAS(测试和设置)是计算机科学中众所周知的原始原子 RMW 操作之一,可以成为互斥(锁定)的构建块。在某些旧硬件(如 Motorola 68000)上,它是唯一的原子 RMW。
没有机器的唯一原子 RMW 是测试和清除。(零初始化内存是常态,因此如果 0 表示解锁,则静态存储中的自旋锁或互斥体将以解锁状态开始,并且 TAS 获取锁。在获取自旋锁时需要原子 RMW,但在释放自旋锁时不需要。)
TAS 还可以通过原子交换(又名交换)来有效实现,其他一些旧硬件将其作为其唯一的原子 RMW 提供。(
local = foo.exchange(true)
并测试结果。)但 TAS 和交换都不能作为任意无锁原子 RMW
fetch_xor
或 CAS(例如比较和交换compare_exchange_weak/strong
)的构建块。仅具有 TAS 或模拟它的方式的机器不能提供 lock-freestd::atomic<bool>
,但可以提供 lock-freestd::atomic_flag
。(LL/SC或 CAS 本身是单个变量上任意无锁 RMW 的构建块。所有(?)支持多核的现代机器都至少具有其中之一,有时还直接支持一些常见的整数运算像x86-64 和 ARMv8.1 上的 、
fetch_add
、等。当然,纯加载和纯存储操作的原子性保证因此可以作为实际加载完成,而不是在只读内存上出错的 RMW并在读者之间产生争论。)fetch_or
exchange
.load()
假设,在只有 test-can-clear 的 CPU 上,您仍然可以使用具有非零对象表示的
atomic_flag
C++状态来实现;false
在 C++20 之前,不需要静态初始化使其为 false。但是,如果 CPU 具有 TAS 和“TAC”(或可以同时执行这两种操作的交换),但没有足够的功能来实现无锁atomic<bool>
,则无法通过 充分利用它atomic_flag
。atomic_flag
与任何atomic<T>
. 它的存在是为了公开一组最小的无锁功能,这些功能可以在能够实现 ISO C++11 的各种硬件上实现(包括std::mutex
非无锁的锁定std::atomic<T>
)。它避免要求一些事情:
只读访问(C++20 之前
.test()
)某些硬件可能具有竞争检测,可能会在并发读 + 写时出现故障。(与 C++ 中对非原子变量的未定义行为相同的条件。)大概这样的硬件将具有允许并发的特殊指令,其中可能仅包括写入和 RMW。
除 . 之外的只写访问
.clear()
。可以使用并忽略返回值来完成写入。但这效率不高。它仍然是一个原子 RMW,因此继续发布序列,并且必须查看修改顺序中的“最新值”,因此它是可线性化的,因此如果返回值不是,编译器将其优化为仅存储是很重要的用过的。 我不知道不提供API 可能存在什么硬件原因。如果底层硬件不允许以普通方式存储除 以外的值(或任何实际位模式),则实现始终可以使用 TAS 指令并忽略结果。变强不是问题。
true
.test_and_set()
.set()
0
clear
因此,这可能只是保持 API 最小化的一个例子,因为它基本上并不重要;大多数代码应该直接使用,
atomic<bool>
因为它在大多数人正在编程的真实平台上也是无锁的。变量值的存储:您总是可以
if(x) flag.TAS(); else flag.clear()
,但 API 不提供这一点。切换现有值:唯一可用的 RMW (TAS) 存储不依赖于已有值的新值。这允许按照ARM
swp
(交换)等指令来实现,该指令早于 ARM 上的 LL/SC 支持。就 TAS 本身而言也是如此。据推测,这是使 TAS 和交换更容易在硬件中实现的因素之一:读+写可能会在同一周期内发生,这与必须根据加载结果计算要写入的值的操作不同,因此不会'至少要等到一个周期后才能准备好。
如果 所支持的任何操作
atomic<bool>
需要互斥体(由于缺少 CAS 或 LL/SC),则所有操作都必须遵守互斥体。(除了纯加载之外,如果撕裂和内存排序不是问题,例如对于某些系统上的字节或布尔或原子 int 的轻松加载,但这是现代编译器可能不关心的过时系统的一个极端情况.) 那么atomic<bool>::is_always_lock_free
必须是false
。C++20 添加了
std::atomic_signed_lock_free
保证std::atomic_unsigned_lock_free
无锁的整数类型(并且对于等待/通知来说是“最有效的”)。这些在独立实现中是可选的(不在操作系统下托管),但我认为这排除了 386 或 68000 上的 C++20 托管实现;您需要 486 或 68020 或更高版本。我认为 C++20 决定添加人们想要的有用的东西,即使这意味着一些复古硬件无法实现 C++20。现代 C++ 一直在其他领域做出类似的选择,例如要求有符号整数为二进制补码,放弃对补码或符号/数值的实现选择。在 ISO C 中,线程/互斥体/原子的东西是可选的,与 ISO C++ 不同,因此现代 C 仍然可以通过完全省略线程支持库来在古老的硬件上实现。
仅具有 TAS 或交换的 ISA
68000 TAS 指令 68000 / http://www.easy68k.com/paulrsm/doc/dpbm68k3.htm
68020 及更高版本有 CAS (甚至68020/030/040上的CAS2,对两个独立内存位置的操作,尽管事实证明非常难以有效支持,因此从后来的 CPU 中删除)。
ARMv5(仅
swp
交换)SparcV8(由https://llvm.org/docs/Atomics.html#atomics-and-codegen提及)
80386:
cmpxchg
在 Pentium 中正式是新的,尽管486 有一个不同的未记录的操作码。(另请参阅回顾计算486 SMP问题,了解一些早期 x86 SMP 系统。)
80386 具有
xchg
(lock
与内存操作数一起使用时,即使没有前缀,无论您是否想要,它也是一个原子 RMW),和//lock bts
测试和设置、测试和重置(清除)或测试和- 补充(翻转)一点,而不干扰周围的位。btr
btc
它还有
lock add
,lock or
/lock and
等(fetch_or
/fetch_and
没有返回值,除了从结果中设置 FLAGS 之外,这样您就可以在结果全零或设置其 MSB 时进行分支。或者低字节的奇偶校验。)lock xadd
是新的486(fetch_add
包括返回值)如果使用 的返回值fetch_or
,编译器必须使用lock cmpxchg
重试循环来实现它。但这些都不足以
cmpxchg
模仿它。没有原子 RMW 的相互排斥?
从技术上讲,互斥是可能的,无需原子 RMW ,只需通过Peterson 算法
seq_cst
进行加载和存储,但这需要一个数组,其中每个可能的线程都有一个条目,并且 C++ 允许在互斥对象已存在并正在使用后启动新线程。所以这并不是真正可行的。 Lamport 的面包店算法对 size = 的数组有相同的要求NUM_THREADS
。C11 和 C++11 没有兴趣尝试在如此古老的 CPU 上支持线程,因为多处理是一件很困难的事情,所以他们继续并要求
atomic_flag
支持无锁 RMW。在单处理器机器上,实现任何原子 RMW 的一种方法就是禁用/重新启用它周围的中断。从某种意义上说,这仍然是无锁的:它不能像实际的自旋锁或互斥锁那样在中断或信号处理程序与主线程之间创建死锁。这需要在托管实现中进行系统调用。
或者,在中断只能发生在指令边界的单处理器机器上,可以使用单条指令来完成。例如,x86
add [mem], eax
是原子的。中断并因此在同一内核上进行上下文切换,但在多核系统中则不会,除非您也使用前缀lock
。(在特定情况下递增 int 是否有效原子?)。因此,一些具有内存目标 RMW 指令的 CISC 可以将这些指令用于只需原子性的操作。其他线程(或中断处理程序)只能在同一核心上运行(因为这是一个单处理器系统或者因为我们只关心中断/信号处理程序)。尽管这些说明没有任何特殊的 RMW 保证。并发写入器,例如 DMA 或其他总线主控 I/O 设备。或其他核心(如果存在)。
一些 ISA(例如 m68k)可以保存执行指令的部分进度并在中断后恢复,从而解决了这个问题。但 x86 不是这样的;中断仅在指令边界处处理。(参见x86 CMPXCHG 是否是原子的,如果是的话为什么需要 LOCK?)
有关的:
atomic_flag是如何实现的?- 对于 x86,当前编译器使用
xchg
它是因为它比lock bts
.clear(memory_order_release)
或 较弱比使用普通的不带or 的效率要高得多。lock btr
mov
mfence
xchg
原子操作需要硬件支持吗?
为什么只有 std::atomic_flag 保证是无锁的?
TAS 指令 68000 - 自旋锁的示例 asm
http://www.easy68k.com/paulrsm/doc/dpbm68k3.htm讨论了 TAS 背后的设计
在特定情况下递增 int 是否有效原子?- 具有缓存的现代微体系结构如何在不锁定整个内存总线的情况下处理原子 RMW,因此单独的内核可以同时在单独的内存位置上执行原子 RMW。
atomic_flag
确实是唯一保证无锁的类型,因此被设计为架构上必须存在的实现原子的最小支持。一般来说,在例如用于嵌入式设备的简约芯片组上,可能只有一个特定指令允许将存储器字设置为特定值并原子地访问该字的先前值。这通常是不对称的,clear 和 set 的值甚至不一定是 0 和 1。有了这些属性,
atomic_flag
就可以用来实现自旋锁,这可能是我们没有操作系统的架构上唯一可能的低级锁结构。n2145论文介绍如下
std::atomic_flag
:由于原子指令的最小集是测试和设置的并且是明确的,因此
std::atomic_flag
只有这两个功能。即使目前不存在具有如此受限指令集的硬件,将来也可能会出现,并且能够运行带有std::atomic_flag
.我认为Nicol Bolas做到了这一点,这可能与atomic_flag 保证无锁这一事实有关,而atomic bool 则不然。
因此,这可能是一个实现限制。