【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();
        }
    }
}

 

 

 


 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM