关于不可知的语言:原子指令

Atomic Instruction

原子指令是什么意思?

以下内容如何变成原子?

TestAndSet

1
2
3
4
5
int TestAndSet(int *x){
   register int temp = *x;
   *x = 1;
   return temp;
}

从软件的angular来看,如果不想使用非阻塞同步原语,那么如何确保指令的原子性?

仅在硬件上可以使用,还是可以使用某些汇编级别的指令优化?


某些机器指令本质上是原子的-例如,在许多体系结构上,读写本机处理器字长的正确对齐值是原子的。

这意味着硬件中断,其他处理器和超线程无法中断读取或存储以及将部分值读取或写入同一位置。

更复杂的事情,例如原子的读写,可以通过明确的原子机器指令来实现,例如在x86上锁定CMPXCHG。

锁定和其他高级构造是基于这些原子基元构建的,这些原子基元通常仅保护单个处理器字。

仅使用读写指针就可以构建一些巧妙的并发算法,例如在单个读取器和写入器之间共享的链接列表中,或者努力地在多个读取器和写入器之间共享。


原子来自希腊语?τομο? (atomos),意思是"不可分割"。 (注意:我不会说希腊语,所以也许它确实是另外一回事,但是大多数以英语为母语的人以词源来解释这种方式。:-)

在计算中,这意味着操作会发生。在完成之前,没有任何可见的中间状态。因此,如果您的CPU被服务硬件(IRQ)中断,或者另一个CPU正在读取相同的内存,则不会影响结果,并且这些其他操作会将其视为已完成或未启动。

举个例子……假设您想为某个变量设置一个变量,但前提是之前没有设置过。您可能倾向于这样做:

1
2
3
4
if (foo == 0)
{
   foo = some_function();
}

但是如果并行运行怎么办?可能是程序将获取foo,将其视为零,与此同时线程2出现并执行相同的操作并将该值设置为某值。回到原始线程中,代码仍然认为foo为零,并且变量被分配了两次。

在这种情况下,CPU提供了一些指令,可以作为原子实体进行比较和条件赋值。因此,测试并设置,比较和交换以及负载链接/存储条件的。您可以使用它们来实现锁定(您的OS和C库已完成此操作。),也可以编写依赖于原语来完成操作的一次性算法。 (这里有很酷的事情要做,但是大多数凡人都避免这样做,因为担心弄错了。)


下面是一些有关原子性的注释,这些注释可能有助于您理解含义。这些注释来自最后列出的资源,如果您需要更详尽的说明,而不是像我所说的点状项目符号,我建议您阅读其中的一些内容。请指出任何错误,以便我纠正。

定义:

  • 在希腊语中表示"不可分割成较小的部分"
  • 总是观察到"原子"操作已完成或未完成,但是
    永远不会半途而废。
  • 原子操作必须完全执行或不执行
    全部。
  • 在多线程方案中,变量从未突变变为
    直接变异,没有"中途变异"值

示例1:原子操作

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

    1
    2
    3
    4
    5
    6
    7
     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:非原子操作:/-操作

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

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

  • 读我
  • 递增/递减读取值
  • 将新值写回我
  • 每个操作都由3个原子操作组成,它们本身不是原子的
  • 两次尝试在单独的线程上递增i可能会交织,从而使增量之一丢失

示例3-非原子操作:大于4字节的值

  • 考虑以下不可变结构:
1
2
3
4
5
6
7
8
9
10
11
  struct MyLong
   {
       public readonly int low;
       public readonly int high;

       public MyLong(int low, int high)
       {
           this.low = low;
           this.high = high;
       }
   }
  • 我们创建的字段具有MyLong类型的特定值:

    1
    2
    3
    MyLong X = new MyLong(0xAAAA, 0xAAAA);  
    MyLong Y = new MyLong(0xBBBB, 0xBBBB);    
    MyLong Z = new MyLong(0xCCCC, 0xCCCC);

  • 我们在没有线程安全的情况下在单独的线程中修改字段:

    1
    2
    X = Y; //Thread 1                                  
    Y = X; //Thread 2
  • 在.NET中,当复制值类型时,CLR不会调用构造函数-它一次将字节移动一次原子操作

  • 因此,两个线程中的操作现在是四个原子操作
  • 如果没有强制执行线程安全性,则数据可能会损坏
  • 请考虑以下操作执行顺序:

    1
    2
    3
    4
    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位的值而未添加某种锁定以使操作原子化的可能性如上所述,可能会导致数据损坏

处理器操作

  • 在所有现代处理器上,只要满足以下条件,就可以假定自然对齐的本机类型的读写是原子的:

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

  • 处理器供应商在《软件开发人员手册》中为每个处理器定义了原子操作。
  • 在单处理器/单核系统中,可以使用标准的锁定技术来防止CPU指令被中断,但这可能效率不高。
  • 如果可能的话,禁用中断是另一个更有效的解决方案
  • 在多处理器/多核系统中,仍然可以使用锁,但是仅使用一条指令或禁用中断并不能保证原子访问
  • 可以通过确保所使用的指令在总线上声明" LOCK"信号来防止系统中其他处理器同时访问内存,从而实现原子性

语言差异

C#

  • C#保证对占用最多4个字节的任何内置值类型的操作都是原子的
  • 值类型超过四个字节(双精度,长整数等)的操作不保证是原子的
  • CLI保证读取和写入值类型的变量(即处理器的自然指针大小的大小(或更小))是原子的

    • 在64位版本的CLR中的64位OS上运行C#之前,原子执行64位双精度和长整数的读写
  • 创建原子操作:

    • .NET促使Interlocked Class作为System.Threading命名空间的一部分
    • 互锁类提供原子操作,例如增量,比较,交换等。
1
2
3
4
5
6
7
using System.Threading;            

int unsafeCount;                          
int safeCount;                          

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

C

  • C标准不保证原子行为
  • 除非编译器或硬件供应商另行指定,否则所有C / C操作都假定为非原子操作-包括32位整数分配
  • 创建原子操作:

    • C 11并发库包括-原子操作库()
    • 原子库提供原子类型作为模板类,可与您想要的任何类型一起使用
    • 对原子类型的操作是原子的,因此是线程安全的

struct AtomicCounter
{

1
2
3
4
5
6
7
8
9
10
11
12
13
   std::atomic< int> value;  

   void increment(){                                    
       ++value;                                
   }          

   void decrement(){                                        
       --value;                                                
   }

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

}

Java

  • Java保证对占用最多4个字节的任何内置值类型的操作都是原子的
  • 分配给易失的多头和双头也保证是原子的
  • Java提供了一个小的类工具包,这些类通过java.util.concurrent.atomic支持对单个变量的无锁线程安全编程。
  • 这提供了基于低级原子硬件原语的无原子锁定操作,例如比较和交换(CAS)-也称为compare and set:

    • CAS形式-布尔compareAndSet(expectedValue,updateValue);

      • 如果此方法当前持有ExpectedValue,则此方法自动将变量设置为updateValue-成功报告为true
1
2
3
4
5
6
7
8
9
10
11
12
13
14
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


当您进行包含共享资源的任何形式的并行处理(包括合作或共享数据的不同应用程序)时,原子性是一个关键概念。

通过一个例子很好地说明了该问题。假设您有两个要创建文件的程序,但前提是该文件尚不存在。这两个程序中的任何一个都可以在任何时间点创建文件。

如果您这样做(我将使用C,因为它就是您的示例中的内容):

1
2
3
4
5
6
 ...
 f = fopen ("SYNCFILE","r");
 if (f == NULL) {
   f = fopen ("SYNCFILE","w");
 }
 ...

您不能确定其他程序在您的可读打开和写入打开之间没有创建文件。

您无法独自执行此操作,您需要操作系统的帮助,该操作系统通常为此目的提供同步原语,或保证是原子性的另一种机制(例如关系数据库,其中的锁定操作是原子的,或者是较低级别的机制,例如处理器的"测试和设置"指令)。


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

因此无法创建自己的testandset函数。 (尽管我不确定是否可以使用内联的asm代码段,并直接使用testandset助记符(可能是该语句只能通过OS特权来完成))

编辑:
根据这篇文章下面的评论,可以直接使用ASM指令(在intel x86上)创建自己的" bittestandset"函数。但是,这些技巧是否也可以在其他处理器上使用尚不清楚。

我坚持我的观点:如果您想做有气氛的事情,请使用OS功能并且不要自己做