原子指令

时间:2009-11-19 09:55:28

标签: language-agnostic synchronization nonblocking atomicity test-and-set

Atomic指令是什么意思?

以下内容如何成为Atomic?

检查并设置

int TestAndSet(int *x){
   register int temp = *x;
   *x = 1;
   return temp;
}

从软件的角度来看,如果不想使用非阻塞同步原语,那么如何确保指令的原子性?是否只能在硬件或某些装配级指令优化中使用?

5 个答案:

答案 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)

以下是我对原子性的一些注释,可能有助于您理解其含义。这些笔记来自最后列出的来源,如果你需要更彻底的解释而不是像我一样的点状子弹,我建议你阅读其中的一些。请指出任何错误,以便我们纠正错误。

定义:

  • 来自希腊语"不能分成更小的部分"
  • " atomic"总是观察到操作是否完成,但是 从来没有完成过。
  • 原子操作必须完全执行或不执行 所有
  • 在多线程场景中,变量从unmutated变为 直接突变,没有中途变异"值

示例1:原子操作

  • 考虑以下不同线程使用的整数:

     int X = 2;
     int Y = 1;
     int Z = 0;
    
     Z = X;  //Thread 1
    
     X = Y;  //Thread 2
    
  • 在上面的示例中,两个线程使用X,Y和Z

  • 每次读写都是原子的
  • 线程将竞争:
    • 如果线程1获胜,则Z = 2
    • 如果线程2获胜,则Z = 1
    • Z肯定会是这两个值中的一个

示例2:非原子操作:++ / - 操作

  • 考虑增量/减量表达式:

    i++;  //increment
    i--;  //decrement
    
  • 操作转换为:

    1. 阅读我
    2. 递增/递减读取值
    3. 将新值写回i
  • 每个操作都由3个原子操作组成,而不是原子本身
  • 两次尝试在不同的线程上递增i可能会交错,使得其中一个增量丢失

示例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>

处理器操作

  • 在所有现代处理器上,您可以假设自然对齐的本机类型的读取和写入都是原子的,只要:

    • 1:内存总线至少与读取或写入的类型一样宽
    • 2:CPU在单个总线事务中读取和写入这些类型,使其他线程无法在半完成状态下看到它们
  • 在x86和X64上,无法保证大于8个字节的读取和写入是原子的

  • 处理器供应商在Software Developer's Manual
  • 中定义每个处理器的原子操作
  • 在单处理器/单核系统中,可以使用标准锁定技术来防止CPU指令被中断,但这可能效率低下
  • 如果可能,禁用中断是另一种更有效的解决方案
  • 在多处理器/多核系统中,仍然可以使用锁,但仅使用单个指令或禁用中断不保证原子访问
  • 可以通过确保使用的指令声明“锁定”来实现原子性。总线上的信号,以防止系统中的其他处理器同时访问内存

语言差异

<强> C#

  • C#保证对任何占用4字节的内置值类型的操作都是原子
  • 对超过四个字节(double,long等)的值类型的操作不保证是原子的
  • CLI保证对处理器的自然指针大小的大小(或更小)的值类型的变量的读写是原子的
    • Ex - 在64位版本的CLR中在64位操作系统上运行C#,以原子方式执行64位双精度和长整数的读写操作
  • 创建原子操作:
    • .NET将Interlocked Class作为System.Threading命名空间
    • 的一部分
    • Interlocked Class提供增量,比较,交换等原子操作。
using System.Threading;             

int unsafeCount;                          
int safeCount;                           

unsafeCount++;                              
Interlocked.Increment(ref safeCount);

<强> C ++

  • C ++标准不保证原子行为
  • 除非编译器或硬件供应商另有规定,否则所有C / C ++操作都被假定为非原子操作 - 包括32位整数赋值
  • 创建原子操作:
    • C ++ 11并发库包括 - Atomic Operations Library()
    • Atomic库提供原子类型作为模板类,可用于任何您想要的类型
    • 对原子类型的操作是原子的,因此是线程安全的
  

struct AtomicCounter
  {

   std::atomic< int> value;   

   void increment(){                                    
       ++value;                                
   }           

   void decrement(){                                         
       --value;                                                 
   }

   int get(){                                             
       return value.load();                                    
   }      
     

}

<强>爪哇

  • Java保证对任何占用4个字节的内置值类型的操作都是原子的
  • 对volatile volatile和double的赋值也保证是原子的
  • Java提供了一个小型工具包,通过java.util.concurrent.atomic
  • 支持对单个变量进行无锁线程安全编程。
  • 这提供了基于低级原子硬件基元的原子无锁操作,例如比较和交换(CAS) - 也称为比较和设置:
    • CAS表单 - boolean compareAndSet(expectedValue,updateValue);
      • 如果变量当前包含expectedValue,则此变量以原子方式将变量设置为updateValue - 成功时报告为true
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();
     }
}

<强>来源
http://www.evernote.com/shard/s10/sh/c2735e95-85ae-4d8c-a615-52aadc305335/99de177ac05dc8635fb42e4e6121f1d2

答案 4 :(得分:-4)

原子性只能由操作系统保证。操作系统使用底层处理器功能来实现此目的。

因此创建自己的testandset函数是不可能的。 (虽然我不确定是否可以使用内联asm片段,并直接使用testandset助记符(可能是这个语句只能通过OS权限来完成))

编辑: 根据本文下面的评论,可以直接使用ASM指令制作自己的“bittestandset”函数(在intel x86上)。但是,如果这些技巧也适用于其他处理器尚不清楚。

我坚持自己的观点:如果你想做有气质的事情,请使用操作系统功能而不要自己动手