原子指令是什么意思?
以下如何成为原子的?
测试和设置
int TestAndSet(int *x){
register int temp = *x;
*x = 1;
return temp;
}
从软件的角度来看,如果不想使用非阻塞同步原语,如何保证指令的原子性?是否只能在硬件或某些汇编级指令优化中使用?
原子指令是什么意思?
以下如何成为原子的?
测试和设置
int TestAndSet(int *x){
register int temp = *x;
*x = 1;
return temp;
}
从软件的角度来看,如果不想使用非阻塞同步原语,如何保证指令的原子性?是否只能在硬件或某些汇编级指令优化中使用?
一些机器指令本质上是原子的——例如,读取和写入正确对齐的本机处理器字长值在许多架构上都是原子的。
这意味着硬件中断、其他处理器和超线程不能中断读取或存储以及读取或写入部分值到同一位置。
更复杂的事情,例如一起原子地读取和写入,可以通过显式的原子机器指令来实现,例如 x86 上的 LOCK CMPXCHG。
锁定和其他高级构造建立在这些原子原语之上,它们通常只保护一个处理器字。
一些聪明的并发算法可以仅使用指针的读取和写入来构建,例如在单个读取器和写入器之间共享的链表中,或者通过努力,多个读取器和写入器。
以下是我关于原子性的一些笔记,可以帮助您理解其含义。这些注释来自最后列出的来源,如果您需要更全面的解释而不是像我这样的点式项目符号,我建议您阅读其中的一些。请指出任何错误,以便我更正。
定义 :
示例 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 位的值而不添加某种锁定以使操作原子化很可能导致上述损坏数据
处理器操作
在所有现代处理器上,您可以假设自然对齐的本机类型的读取和写入是原子的,只要:
在 x86 和 X64 上,不能保证大于 8 个字节的读写是原子的
语言差异
C#
using System.Threading; int unsafeCount; int safeCount; unsafeCount++; Interlocked.Increment(ref safeCount);
C++
结构原子计数器
{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(); } }
原子来自希腊语ἄτομος(atomos),意思是“不可分割的”。 (警告:我不会说希腊语,所以也许这真的是别的东西,但大多数说词源的说英语的人都是这样解释的。:-)
在计算中,这意味着操作发生了。在完成之前没有任何可见的中间状态。因此,如果您的 CPU 被中断以服务硬件 (IRQ),或者另一个 CPU 正在读取相同的内存,则不会影响结果,并且这些其他操作将观察它是否已完成或未启动。
举个例子……假设你想为某个东西设置一个变量,但前提是它之前没有设置过。你可能倾向于这样做:
if (foo == 0)
{
foo = some_function();
}
但是,如果这是并行运行的呢?可能是程序将 fetch foo
,将其视为零,同时线程 2 出现并执行相同的操作并将值设置为某个值。回到原来的线程,代码仍然认为foo
是零,变量被分配了两次。
对于这种情况,CPU 提供了一些指令,可以将比较和条件赋值作为原子实体进行。因此,测试和设置、比较和交换以及加载链接/存储条件。您可以使用这些来实现锁定(您的操作系统和 C 库已经完成了此操作。)或者您可以编写依赖原语做某事的一次性算法。(这里有一些很酷的事情要做,但大多数凡人都避免这样做,因为害怕弄错。)
当您拥有包括共享资源的任何形式的并行处理(包括不同的应用程序合作或共享数据)时,原子性是一个关键概念。
用一个例子很好地说明了这个问题。假设您有两个程序想要创建一个文件,但前提是该文件尚不存在。这两个程序中的任何一个都可以在任何时间点创建文件。
如果你这样做(我将使用 C,因为它是你的示例中的内容):
...
f = fopen ("SYNCFILE","r");
if (f == NULL) {
f = fopen ("SYNCFILE","w");
}
...
您不能确定其他程序没有在您打开读取和打开写入之间创建文件。
您自己无法做到这一点,您需要操作系统的帮助,通常为此目的提供同步原语,或者保证是原子的其他机制(例如,锁定操作是原子的关系数据库,或较低级别的机制,如处理器“测试和设置”指令)。
原子性只能由操作系统来保证。操作系统使用底层处理器功能来实现这一点。
所以创建自己的 testandset 函数是不可能的。(虽然我不确定是否可以使用内联 asm 片段,并直接使用 testandset 助记符(可能此语句只能使用 OS 权限完成))
编辑:根据这篇文章下面的评论,可以直接使用 ASM 指令制作自己的“bittestandset”函数(在英特尔 x86 上)。但是,这些技巧是否也适用于其他处理器尚不清楚。
我坚持我的观点:如果你想做大气的事情,使用操作系统的功能,不要自己做