我正在尝试理解原子和非原子操作。关于操作系统以及关于C. 根据维基百科页面here
考虑一个简单的计数器,不同的过程可以增加 非原子
天真的,非原子的实施:
读取内存位置的值;
将值加1;
将新值写回内存位置。
现在,想象两个进程正在运行递增单个共享内存位置:
第一个进程读取内存位置的值;
第一个过程为值添加一个;
但是在它将新值写回内存位置之前它被暂停,并允许第二个进程运行:
第二个进程读取内存位置的值,与第一个进程读取的值相同;
第二个过程为该值添加一个;
第二个进程将新值写入内存位置。
如何将上述操作作为一个大气操作。 我对原子操作的理解是任何不间断执行的东西都是原子的。 例如,
int b=1000;
b+=1000;
根据我的理解应该是一个原子操作,因为两个指令都是在没有中断的情况下执行的,我从某个人那里学到了在C中没有任何已知的原子操作,因此上述两个语句都是非原子的。 所以我想要理解的是,当涉及到编程语言而不是操作系统时,原子性是什么?
答案 0 :(得分:4)
C99没有任何方法可以使变量相对于其他线程具有原子性。 C99没有多线程执行的概念。因此,您需要使用特定于编译器的扩展和/或CPU级指令来实现原子性。
下一个C标准,目前称为C1x,将包括原子操作。
即便如此,纯粹的原子性只能保证操作是原子的,它不能保证该操作何时对其他CPU可见。为了实现可见性保证,在C99中,您需要研究CPU的内存模型,并可能使用称为栅栏或内存屏障的特殊CPU指令。您还需要使用某些特定于编译器的编译器屏障告诉编译器。 C1x定义了几个内存排序,当你使用原子操作时,你可以决定使用哪个内存排序。
一些例子:
/* NOT atomic */
b += 1000;
/* GCC-extension, only in newish GCCs
* requirements on b's loads are CPU-specific
*/
__sync_add_and_fetch(&b, 1000);
/* GCC-extension + x86-assembly,
* b should be aligned to its size (natural alignment),
* or loads will not be atomic
*/
__asm__ __volatile__("lock add $1000, %0" : "+r"(b));
/* C1x */
#include <stdatomic.h>
atomic_int b = ATOMIC_INIT(1000);
int r = atomic_fetch_add(&b, 1000) + 1000;
所有这些都看起来很复杂,所以你应该坚持使用互斥体,这会让事情变得更容易。
答案 1 :(得分:3)
int b = 1000;
b+=1000;
在指令级别变成多个语句。至少,准备一个寄存器或存储器,分配1000,然后获取该寄存器/存储器的内容,向内容添加1000,并将新值(2000)重新分配给该寄存器。如果没有锁定,操作系统可以在该操作的任何时刻挂起进程/线程。此外,在多进程系统上,当您的操作正在进行时,不同的处理器可以访问该内存(在这种情况下不会是寄存器)。
当您取消锁定时(这就是如何使其成为原子),您在某种程度上告知操作系统暂停此进程/线程并且不应该访问此内存其他过程。
现在上面的代码可能会被编译器优化为2000到b的内存位置的简单赋值,但是我忽略了这个答案的目的。
答案 2 :(得分:3)
b+=1000
在我知道的所有系统上编译为多个指令。因此它不是原子的。
即使b=1000
也可以是非原子的,尽管你必须努力构建一个不是原子的情况。
事实上,C没有线程概念,所以在C中没有什么是原子的。你需要依赖编译器和工具的特定于实现的细节。
答案 3 :(得分:0)
上面的语句是非原子的,因为它成为一个移动指令,将b加载到寄存器中(如果它不是),然后向它添加1000并将存储重新加入内存。许多指令集允许原子性通过原子增量最简单的x86与lock addl dest,src;其他一些指令集使用cmpxchg来实现相同的结果。
答案 4 :(得分:0)
所以我想要了解的是什么 原子性是不同的 来自编程语言而不是 操作系统?
我对这个问题感到有些困惑。你到底是什么意思?原子性概念在prog中都是相同的。语言和操作系统。
关于原子性和语言,这里有一个关于JAVA中的原子性的链接,可能会给你一个不同的视角:What operations in Java are considered atomic?