该代码通过打印在“危险上下文”中不断变化的两个值,试图显示涉及在文本程序的非线性开发(异步性)中共享访问变量的问题。
#include <signal.h>
#include <stdio.h>
struct two_int { int a, b; } data;
void signal_handler(int signum){
printf ("%d, %d\n", data.a, data.b);
alarm (1);
}
int main (void){
static struct two_int zeros = { 0, 0 }, ones = { 1, 1 };
signal (SIGALRM, signal_handler);
data = zeros;
alarm (1);
while (1){
data = zeros;
data = ones;
}
}
当我尝试运行代码时出现了问题(或者更好的是,没有出现)。我在默认配置中使用的是gcc版本6.3.0 20170516(Debian 6.3.0-18 + deb9u1)。不会产生误导的输出。获取“错误”对值的频率为0!到底发生了什么?为什么使用静态全局变量重新输入没有问题?
那不是真的-entrancy;您不会在同一线程(或不同线程)中两次运行函数。您可以通过递归或将当前函数的地址作为回调函数指针arg传递给另一个函数来获得。 (它不会不安全,因为它是同步的。)
这只是信号处理程序和主线程之间的普通香草数据争用UB(未定义行为):为此,仅sig_atomic_t
被保证是安全的。其他的可能会起作用,例如在x86-64上可以用一条指令加载或存储8字节对象的情况下,编译器恰好选择了该asm。 (如@icarus的答案所示)。
您由于数据争用UB而实际发生的撕裂测试用例可能是在32位模式下开发的,或经过测试,或者是使用较旧的dudu编译器单独加载了结构成员。]
在您的情况下,编译器可以从无限循环中优化存储,因为没有UB-free程序无法观察到它们。 data
不是_Atomic
或volatile
long long
,并且gcc在循环之前使用了单个movdqa
16字节存储。 (这不是guaranteed原子,但实际上在几乎所有CPU上都是假定它是对齐的,或者在Intel上根本没有越过缓存行边界。Why is integer assignment on a naturally aligned variable atomic on x86?)因此启用优化的编译也会破坏您的测试,并每次都显示相同的值。 C不是可移植的汇编语言。volatile struct two_int
也将强制编译器不对其进行优化,但是
not
强制其以原子方式加载/存储整个结构。 (不过,它也不会[[stop这样做。)请注意,volatile
确实not避免了数据争用UB,但实际上,它足以进行线程间通信,这是人们的方式在C11 / C ++ 11之前,为常规CPU体系结构构建了手动滚动原子(以及内联asm)。它们是高速缓存一致的,因此对于纯负载和纯存储,volatile
为in practice mostly similar to _Atomic
with memory_order_relaxed
,如果用于的类型足够窄,则编译器将使用一条指令,因此您不会感到费解。当然,_Atomic
对ISO C标准没有任何保证,与编写使用memory_order_relaxed
和mo_relaxed编译为相同asm的代码相比。和
异步运行的volatile
或_Atomic
执行了global_var++;
,那将是一种使用重入的方法创建数据竞赛UB。取决于它是如何编译的(到内存目标inc或添加,或分开加载/ inc /存储),对于同一线程中的信号处理程序,它是否是原子的。有关x86和C ++中原子性的更多信息,请参见int
。 (C11的long long
和Can num++ be atomic for 'int num'?属性提供与C ++ 11的stdatomic.h
模板相同的功能)在指令中间不会发生中断或其他异常,因此内存目标地址是原子wrt。上下文在单核CPU上切换。在单核CPU上,只有(缓存相关的)DMA写入器可以“踩”从_Atomic
开始的增量而没有std::atomic<T>
前缀。没有其他线程可以在其上运行的任何其他核心。因此,它类似于信号的情况:信号处理程序运行
代替
处理信号的线程的正常执行,因此不能在一条指令的中间对其进行处理。