【C#】 atomic action原子操作|primitive(基元、原语)

概念

原子操作(atomic action):也叫primitive(原语、基元),它是操作系统用语范畴。指由若干条指令组成的,用于完成一定功能的一个过程。  原语是由若干个机器指令构成的完成某种特定功能的一段程序,具有不可分割性·即原语的执行必须是连续的,在执行过程中不允许被中断。

操作系统只需在执行以下操作时暂时屏蔽全部中断:测试信号量、更新信号量以及在需要时使某个进程睡眠。由于这些动作只需要几条指令,所以屏蔽中断不会带来什么副作用。如果使用多个CPU,则每个信号量应由一个锁变量进行保护。

在.net中实现原子操作的类是 Interlocked类。CAS在.NET中的实现类是Interlocked

Interlocked类主要方法

interlocked是基于CAS操作

CAS操作基于CPU提供的原子操作指令实现。只能保证共享变量操作的原子:

对于Intel X86处理器,可通过在汇编指令前增加LOCK前缀来锁定系统总线,使系统总线在汇编指令执行时无法访问相应的内存地址。而各个编译器根据这个特点实现了各自的原子操作函数。

 CAS是一种有名的无锁算法。无锁编程(指C#代码中不加锁,汇编代码会自动加锁),即不适用锁的情况下实现多线程之间的变量同步,也就是在没有现成被阻塞的情况下实现变量的同步。

CompareExchange(ref a ,b,c):比较吧a,c是否相等,如果相等,则用b替换a的值。
CompareExchange<T>(T, T, T)  比较两个指定的引用类型的实例 T 是否相等,如果相等,则替换第一个。好多原子操作都是基于这个函数实现的。
Decrement(): 安全递减1,相当于 i--
Exchange(): 安全交换数据,相当于 a = 30
Increment() :安全递加1,相当于 i++
Add() :安全相加一个数值,相当于 a = a + 3
Read() : 安全读取数值,相等于int a=b

案例:用5个线程从0数到1亿

 

using System.Diagnostics;

class Program
{
    static long counter = 1;
    /// <summary>
    /// 开5个线程 从0数到1亿
    /// </summary>
    /// <param name="args"></param>
    static void Main(string[] args)
    {
        Stopwatch stopwatch = new Stopwatch();
        stopwatch.Start();
         Parallel.Invoke(f1, f1, f1, f1, f1);
       // f1();
        Console.WriteLine(stopwatch.ElapsedMilliseconds);
        Console.WriteLine(counter);
    }
    static void f1()
    {

        for (int i = 1; i <= 10_000_000; i++)
        {
          Interlocked.Increment(ref counter);
       //  counter++;
        }
    }

}
//5个线程花费时间  1608ms


//单线程 125 ms
 

本以为5个线程会更快,结果还不如一个线程快。这是什么问题???

因为Interlocked.Increment是采用CAS操作

CAS是一种有名的无锁算法。无锁编程(指编程语言方面),即不适用锁的情况下实现多线程之间的变量同步,也就是在没有现成被阻塞的情况下实现变量的同步。

CAS原理

CAS,是“Compare And Swap”的缩写,中文简称就是“比较并替换”。

在这个机制中有三个核心的参数:

  • 主内存中存放的共享变量的值:V(一般情况下这个V是内存的地址值,通过这个地址可以获得内存中的值)
  • 工作内存*享变量的副本值,也叫预期值:A
  • 需要将共享变量更新到的最新值:B

【C#】 atomic action原子操作|primitive(基元、原语)

 

如上图中,主存中保存V值,线程中要使用V值要先从主存中读取V值到线程的工作内存A中,然后计算后变成B值,最后再把B值写回到内存V值中。多个线程共用V值都是如此操作。CAS的核心是在将B值写入到V之前要比较A值和V值是否相同,如果不相同证明此时V值已经被其他线程改变,重新将V值赋给A(自旋),并重新计算得到B,如果相同,则将B值赋给V。

值得注意的是CAS机制中的这步步骤是原子性的(从cpu指令层面提供的原子操作),所以CAS机制可以解决多线程并发编程对共享变量读写的原子性问题。

CAS的适用场景

读多写少:如果有大量的写操作,CPU开销可能会过大,因为冲突失败后会不断重试(自旋),这个过程中会消耗CPU

cas操作多出比较和写入内存,所以要耗费太多时间了。而单线程不用Interlocked 直接用缓存的数据进行累加,所以单线程更快。

上一篇:JS读取/创建本地文件及目录文件夹的方法


下一篇:C# 子窗体调用父窗体的方法