【C# 线程】并发编程的基石——CAS机制

其实Java并发框架的基石一共有两块,一块是本文介绍的CAS,另一块就是AQS,后续也会写博客介绍。

什么是CAS机制

CAS机制是一种数据更新的方式。在具体讲什么是CAS机制之前,我们先来聊下在多线程环境下,对共享变量进行数据更新的两种模式:悲观锁模式和乐观锁模式。

悲观锁更新的方式认为:在更新数据的时候大概率会有其他线程去争夺共享资源,所以悲观锁的做法是:第一个获取资源的线程会将资源锁定起来,其他没争夺到资源的线程只能进入阻塞队列,等第一个获取资源的线程释放锁之后,这些线程才能有机会重新争夺资源。synchronized就是java中悲观锁的典型实现,synchronized使用起来非常简单方便,但是会使没争抢到资源的线程进入阻塞状态,线程在阻塞状态和Runnable状态之间切换效率较低(比较慢)。比如你的更新操作其实是非常快的,这种情况下你还用synchronized将其他线程都锁住了,线程从Blocked状态切换回Runnable华的时间可能比你的更新操作的时间还要长。

乐观锁更新方式认为:在更新数据的时候其他线程争抢这个共享变量的概率非常小,所以更新数据的时候不会对共享数据加锁。但是在正式更新数据之前会检查数据是否被其他线程改变过,如果未被其他线程改变过就将共享变量更新成最新值,如果发现共享变量已经被其他线程更新过了,就重试,直到成功为止。CAS机制就是乐观锁的典型实现。

CAS,是Compare and Swap的简称,在这个机制中有三个核心的参数:

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

 

 

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

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

ABA问题

所谓ABA问题, 就是比较并交换的循环,存在一个时间差,而这个时间差可能带来意想不到的问题。
比如有两个线程A、B:
    一开始都从主内存中拷贝了原值为3;
    A线程执行到var5=this.getIntVolatile,即var5=3。此时A线程挂起;
    B修改原值为4,B线程执行完毕;
    然后B觉得修改错了,然后再重新把值修改为3;
    A线程被唤醒,执行this.CompareTxchange( )方法,发现这个时候主内存的值等于快照值3,(但是却不知道B曾经修改过),修改成功。
尽管线程A CAS操作成功,但不代表就没有问题。有的需求,比如CAS,只注重头和尾,只要首尾一致就接受。但是有的需求,还看重过程,中间不能发生任何修改。这就引出了原子引用。

原子引用
Int32对整数进行原子操作,如果是一个普通的对象呢?可以用 Interlocked.CompareExchange<T>(T, T, T)泛型来包装这个普通类,使其操作原子化。

C# 对CAS的ABA问题的解决方案

C#,通过Interlocked方法实现。CAS在.NET中的实现类是Interlocked,内部提供很多原子操作的方法,最终都是调用Interlocked.CompareExchange()
Windows,通过Windows API实现了InterlockedCompareExchangeXYZ系列函数。

CAS机制优缺点

CAS的适用场景

读多写少:如果有大量的写操作,CPU开销可能会过大,因为冲突失败后会不断重试(自旋),这个过程中会消耗CPU
单个变量原子操作:CAS机制所保证的只是一个变量的原子操作。

CAS总结

任何技术都不是完美的,当然,CAS也有他的缺点:
CAS实际上是一种自旋锁,
一直循环,开销比较大。
只能保证一个变量的原子操作,多个变量依然要加锁。
引出了ABA问题(C# Interlocked.CompareExchange()方法 可解决)。
而他的使用场景适合在一些并发量不高、线程竞争较少的情况,加锁太重。但是一旦线程冲突严重的情况下,循环时间太长,为给CPU带来很大的开销。

CAS机制的案例:

下面的基本示例展示了无锁堆栈中的 SpinWait 结构。 如果需要高性能的线程安全堆栈,请考虑使用 System.Collections.Concurrent.ConcurrentStack<T>

详解:启用3个线程给自定义堆栈LockFreeStack<T>的 字段reeStac 添加数据(0-20)。用到cas 技术保证了线程的同步。

LockFreeStack<int> reeStac = new();

for (int i = 1; i <=3; i++)
{
    Thread se = new Thread(test);
    se.Start();
}

    void test(){


    for (int i = 0; i < 20; i++)
    {
        reeStac.Push(i);

    }

}


public class LockFreeStack<T>
{
    private volatile Node m_head;

    private class Node { public Node Next; public T Value; }

    public void Push(T item)
    {
        var spin = new SpinWait();
        Node node = new Node { Value = item }, head ;
        while (true)
        {
            head = m_head;
            node.Next = head;
            Console.WriteLine("Processor:{0},Thread{1},priority:{2} count:{3} ", Thread.GetCurrentProcessorId(), Thread.CurrentThread.ManagedThreadId, Thread.CurrentThread.Priority,item );
            Node dd = Interlocked.CompareExchange(ref m_head, node, head);//如果相等 就把node赋值给m_head,返回值都是原来的m_head。
            if (dd == head) break;//判断是否赋值成功。成功就跳出死循环。
            spin.SpinOnce();
            Console.WriteLine("Processor:{0},Thread{1},priority:{2}  spin.SpinOnce()", Thread.GetCurrentProcessorId(), Thread.CurrentThread.ManagedThreadId, Thread.CurrentThread.Priority);
        }
    }

    public bool TryPop(out T result)
    {
        result = default(T);
        var spin = new SpinWait();

        Node head;
        while (true)
        {
            head = m_head;
            if (head == null) return false;
            if (Interlocked.CompareExchange(ref m_head, head.Next, head) == head)
            {
                result = head.Value;
                return true;
            }
            spin.SpinOnce();
        }
    }
}

 

 

 


 

posted @ 2021-12-31 01:44  小林野夫  阅读(1225)  评论(0编辑  收藏  举报
原文链接:https://www.cnblogs.com/cdaniu/