[英]Making data reads/writes atomic in C11 GCC using <stdatomic.h>?
对于 C11 原子,您甚至不必使用函数。 如果您的实现(= 编译器)支持原子,您只需在变量声明中添加一个原子说明符,然后对它的所有操作都是原子的:
_Atomic(int) toto = 65;
...
toto += 2; // is an atomic read-modify-write operation
...
if (toto == 67) // is an atomic read of toto
Atomics 有其价格(它们需要更多的计算资源),但只要您几乎不使用它们,它们就是同步线程的完美工具。
如果我目前在一个线程中有一个 int 赋值:messageBox[i] = 2,我如何使这个赋值原子化? 同样用于阅读测试,例如 if (messageBox[i] == 2)。
你几乎不需要做任何事情。 在几乎所有情况下,您的线程共享(或与之通信)的数据都受到保护,不会通过互斥锁、信号量等事物进行并发访问。 基本操作的实现保证了内存的同步。
这些原子的原因是帮助您在代码中构建更安全的竞争条件。 它们有许多危险; 包含:
ai += 7;
如果适当地定义了ai,将使用原子协议。 试图破译竞争条件并不能通过模糊实现来帮助。
他们还有一个高度依赖机器的部分。 例如,上面的行在某些平台上可能会失败[1],但是这种失败是如何反馈给程序的呢? 不是[2]。
只有一种操作可以选择处理失败; atomic_compare_exchange_(弱|强)。 Weak 只尝试一次,让程序选择如何以及是否重试。 强重试无休止。 仅仅尝试一次是不够的——由于中断可能会发生虚假故障——但对非虚假故障进行无休止的重试也不好。
可以说,对于健壮的程序或广泛适用的库,您应该使用的唯一位是 atomic_compare_exchange_weak()。
[1] 加载链接、条件存储 (ll-sc) 是在异步总线架构上进行原子事务的常用方法。 加载链接在缓存行上设置一个小标志,如果任何其他总线代理尝试修改该缓存行,该标志将被清除。 如果在缓存中设置了 little 标志,Store-conditional 存储一个值,并清除该标志; 如果标志被清除,Store-conditional 会发出错误信号,因此可以尝试适当的重试操作。 从这两个操作中,您可以在完全异步的总线架构上构建您喜欢的任何原子操作。
ll-sc 可能对位置的缓存属性有微妙的依赖性。 允许的缓存属性取决于平台,因为可以在 ll 和 sc 之间执行哪些操作。
如果您对缓存不佳的访问进行 ll-sc 操作,然后盲目重试,您的程序将锁定。 这不仅仅是猜测; 我不得不在基于 ARMv7 的“安全”系统上调试其中之一。
[2]:
#include <stdatomic.h>
int f(atomic_int *x) {
return (*x)++;
}
f:
dmb ish
.L2:
ldrex r3, [r0]
adds r2, r3, #1
strex r1, r2, [r0]
cmp r1, #0
bne .L2 /* note the retry loop */
dmb ish
mov r0, r3
bx lr
假设多线程应用程序中的数据读/写在操作系统/硬件级别是原子的是不安全的,并且可能导致数据损坏
实际上,像int
这样的类型的非复合操作在所有合理的架构上都是原子的。 你读到的只是一个骗局。
(增量是一个复合操作:它有一个读、一个计算和一个写组件。每个组件都是原子的,但整个复合操作不是。)
但硬件级别的原子性不是问题。 您使用的高级语言根本不支持对常规类型的这种操作。 您需要使用原子类型甚至有权以原子性问题相关的方式操作对象:当您可能修改另一个线程中正在使用的对象时。
(或 volatile 类型。但不要使用 volatile。使用原子。)
声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.