Atomic指令是什么意思?
以下内容如何成为Atomic?
检查并设置
int TestAndSet(int *x){
register int temp = *x;
*x = 1;
return temp;
}
从软件的角度来看,如果不想使用非阻塞同步原语,那么如何确保指令的原子性?是否只能在硬件或某些装配级指令优化中使用?
答案 0 :(得分:8)
某些机器指令本质上是原子的 - 例如,在许多架构上读取和写入本机处理器字大小的正确对齐值是原子。
这意味着硬件中断,其他处理器和超线程不能中断读取或存储,读取或写入部分值到同一位置。
通过显式原子机指令可以实现更复杂的事物,例如原子地一起读取和写入。在x86上锁定CMPXCHG。
锁定和其他高级构造构建在这些原子基元上,通常只保护单个处理器字。
可以仅使用指针的读取和写入来构建一些聪明的并发算法,例如链接列表在单个读者和作者之间共享,或者与努力,多个读者和作者共享。
答案 1 :(得分:3)
Atomic来自希腊语ἄτομος(atomos),意思是“不可分割的”。 (警告:我不会说希腊语,所以也许它真的是别的,但是大多数英语使用者都会用这种方式解释它。: - )
在计算中,这意味着操作,发生。在完成之前没有任何可见的中间状态。因此,如果您的CPU被服务硬件(IRQ)中断,或者另一个CPU正在读取相同的内存,它不会影响结果,而这些其他操作会将其视为已完成或未启动。
作为一个例子......假设您想要将变量设置为某个变量,但前提是它尚未设置。您可能倾向于这样做:
if (foo == 0)
{
foo = some_function();
}
但如果这是并行运行怎么办?可能是程序将获取foo
,将其视为零,同时线程2出现并执行相同的操作并将值设置为某些内容。回到原始线程,代码仍然认为foo
为零,并且变量被分配两次。
对于这样的情况,CPU提供了一些可以进行比较的指令和作为原子实体的条件赋值。因此,测试和设置,比较和交换,以及负载链接/存储条件。您可以使用它们来实现锁定(您的操作系统和C库已完成此操作。)或者您可以编写依赖基元执行某些操作的一次性算法。 (这里有很酷的事情要做,但是大多数凡人都会因为害怕错误而避免这种情况。)
答案 2 :(得分:1)
当您进行包含共享资源的任何形式的并行处理(包括合作或共享数据的不同应用程序)时,原子性是一个关键概念。
通过一个例子可以很好地说明这个问题。假设您有两个要创建文件的程序,但前提是该文件尚不存在。这两个程序中的任何一个都可以在任何时间点创建文件。
如果你这样做(我将使用C,因为它就是你的例子):
...
f = fopen ("SYNCFILE","r");
if (f == NULL) {
f = fopen ("SYNCFILE","w");
}
...
您无法确定其他程序是否在您打开的读取和打开以进行写入之间创建了文件。
你无法独自完成这项任务,你需要操作系统的帮助,通常为此目的提供同步原语,或者保证是原子的另一种机制(例如锁定操作的关系数据库)是原子的,或者是处理器“测试和设置”指令的低级机制。
答案 3 :(得分:0)
以下是我对原子性的一些注释,可能有助于您理解其含义。这些笔记来自最后列出的来源,如果你需要更彻底的解释而不是像我一样的点状子弹,我建议你阅读其中的一些。请指出任何错误,以便我们纠正错误。
定义:
示例1:原子操作
考虑以下不同线程使用的整数:
int X = 2;
int Y = 1;
int Z = 0;
Z = X; //Thread 1
X = Y; //Thread 2
在上面的示例中,两个线程使用X,Y和Z
示例2:非原子操作:++ / - 操作
考虑增量/减量表达式:
i++; //increment
i--; //decrement
操作转换为:
示例3 - 非原子操作:大于4字节的值
struct MyLong { public readonly int low; public readonly int high; public MyLong(int low, int high) { this.low = low; this.high = high; } }
我们创建具有MyLong类型的特定值的字段:
MyLong X = new MyLong(0xAAAA, 0xAAAA); MyLong Y = new MyLong(0xBBBB, 0xBBBB); MyLong Z = new MyLong(0xCCCC, 0xCCCC);
我们在没有线程安全的情况下在单独的线程中修改字段:
X = Y; //Thread 1 Y = X; //Thread 2
在.NET中,复制值类型时,CLR不会调用构造函数 - 它一次将字节移动一个原子操作
请考虑以下执行操作顺序:
X.low = Y.low; //Thread 1 - X = 0xAAAABBBB Y.low = Z.low; //Thread 2 - Y = 0xCCCCBBBB Y.high = Z.high; //Thread 2 - Y = 0xCCCCCCCC X.high = Y.high; //Thread 1 - X = 0xCCCCBBBB <-- corrupt value for X
在32位操作系统上的多个线程上读取和写入大于32位的值而不添加某种锁定以使操作原子化可能导致上述损坏的数据
< / LI>处理器操作
在所有现代处理器上,您可以假设自然对齐的本机类型的读取和写入都是原子的,只要:
在x86和X64上,无法保证大于8个字节的读取和写入是原子的
语言差异
<强> C#强>
using System.Threading; int unsafeCount; int safeCount; unsafeCount++; Interlocked.Increment(ref safeCount);
<强> C ++ 强>
struct AtomicCounter
{std::atomic< int> value; void increment(){ ++value; } void decrement(){ --value; } int get(){ return value.load(); }
}
<强>爪哇强>
import java.util.concurrent.atomic.AtomicInteger; public class Counter { private AtomicInteger value= new AtomicInteger(); public int increment(){ return value.incrementAndGet(); } public int getValue(){ return value.get(); } }
答案 4 :(得分:-4)
原子性只能由操作系统保证。操作系统使用底层处理器功能来实现此目的。
因此创建自己的testandset函数是不可能的。 (虽然我不确定是否可以使用内联asm片段,并直接使用testandset助记符(可能是这个语句只能通过OS权限来完成))
编辑: 根据本文下面的评论,可以直接使用ASM指令制作自己的“bittestandset”函数(在intel x86上)。但是,如果这些技巧也适用于其他处理器尚不清楚。
我坚持自己的观点:如果你想做有气质的事情,请使用操作系统功能而不要自己动手