1. 程式人生 > >C#SpinWait和volatile一點溫習

C#SpinWait和volatile一點溫習

枚舉類型 代碼 針對 如果 mom spi 一段 volatile spa

今天看ConcurrentQueue<T> 源碼發現裏面居然沒有用到lock,我記得ConcurrentDictionary裏面是有lock的,lock的是字典裏面每一個key,但是ConcurrentQueue<T> 的線程安全確是用SpinWait對象和volatile關鍵字來實現,於是乎就溫習了一下,直接上code

 class Test
    {
        /*
        volatile多用於多線程的環境,當一個變量定義為volatile時,讀取這個變量的值時候每次都是從momery裏面讀取而不是從cache讀。
        這樣做是為了保證讀取該變量的信息都是最新的,而無論其他線程如何更新這個變量。

        volatile 修飾符通常用於由多個線程訪問但不使用 lock 語句對訪問進行序列化的字段。
        volatile 關鍵字可應用於以下類型的字段:
        引用類型。
        指針類型(在不安全的上下文中)。 請註意,雖然指針本身可以是可變的,但是它指向的對象不能是可變的。 換句話說,您無法聲明“指向可變對象的指針”。
        類型,如 sbyte、byte、short、ushort、int、uint、char、float 和 bool。
        具有以下基類型之一的枚舉類型:byte、sbyte、short、ushort、int 或 uint。
        已知為引用類型的泛型類型參數。
        IntPtr 和 UIntPtr。
        可變關鍵字僅可應用於類或結構字段。 不能將局部變量聲明為 volatile。
        
*/ static volatile bool _isCompleted = false; static void UserModeWait() { while (!_isCompleted) { Console.Write("②."); } Console.WriteLine(); Console.WriteLine("③等待完成"); } static void
HybridSpinWait() { /* 自旋等待 一個輕量同步類型(結構體),提供對基於自旋的等待的支持。SpinWait只有在多核處理器下才具有使用意義。在單處理器下,自旋轉會占據CPU時間,卻做不了任何事。 SpinWait並沒有設計為讓多個任務或線程並發使用。因此,如果多個任務或者線程通過SpinWait的方法進行自旋,那麽每一個任務或線程都應該使用自己的SpinWait實例。 */ var w = new SpinWait();
while (!_isCompleted) { // 執行單一自旋。 w.SpinOnce(); /* 判斷對SpinWait.SpinOnce() 的下一次調用是否觸發上下文切換和內核轉換。 由NextSpinWillYield屬性代碼可知,若SpinWait運行在單核計算機上,它總是進行上下文切換(讓出處理器)。 SpinWait不僅僅是一個空循環。它經過了精心實現,可以針對一般情況提供正確的旋轉行為以避免內核事件所需的高開銷的上下文切換和內核轉換; 在旋轉時間足夠長的情況下自行啟動上下文切換,SpinWait甚至還會在多核計算機上產生線程的時間片(Thread.Yield())以防止等待線程阻塞高優先級的線程或垃圾回收器線程。 */ Console.WriteLine("是否觸發上下文切換和內核轉換: " + w.NextSpinWillYield); } Console.WriteLine("③等待完成"); } public static void RunTest() { var t1 = new Thread(UserModeWait); var t2 = new Thread(HybridSpinWait); Console.WriteLine("①運行用戶模式等待"); t1.Start(); //將當前執行RunTest()方法線程掛起指定的時間。 讓t1線程執行輸出②. Thread.Sleep(1); _isCompleted = true; //將當前線程阻塞指定的時間 Thread.Sleep(TimeSpan.FromSeconds(1)); _isCompleted = false; Console.WriteLine("①運行混合SpinWait構造 等待"); t2.Start(); Thread.Sleep(5); _isCompleted = true; Console.ReadKey(); } }

在網上找了一段java的描述

恐怕比較一下volatile和synchronized的不同是最容易解釋清楚的。volatile是變量修飾符,而synchronized則作用於一段代碼或方法;看如下三句get代碼:

  1. int i1; int geti1() {return i1;}
  2. volatile int i2; int geti2() {return i2;}
  3. int i3; synchronized int geti3() {return i3;}

  geti1()得到存儲在當前線程中i1的數值。多個線程有多個i1變量拷貝,而且這些i1之間可以互不相同。換句話說,另一個線程可能已經改 變了它線程內的i1值,而這個值可以和當前線程中的i1值不相同。事實上,Java有個思想叫“主”內存區域,這裏存放了變量目前的“準確值”。每個線程 可以有它自己的變量拷貝,而這個變量拷貝值可以和“主”內存區域裏存放的不同。因此實際上存在一種可能:“主”內存區域裏的i1值是1,線程1裏的i1值 是2,線程2裏的i1值是3——這在線程1和線程2都改變了它們各自的i1值,而且這個改變還沒來得及傳遞給“主”內存區域或其他線程時就會發生。
   而geti2()得到的是“主”內存區域的i2數值。用volatile修飾後的變量不允許有不同於“主”內存區域的變量拷貝。換句話說,一個變量經 volatile修飾後在所有線程中必須是同步的;任何線程中改變了它的值,所有其他線程立即獲取到了相同的值。理所當然的,volatile修飾的變量 存取時比一般變量消耗的資源要多一點,因為線程有它自己的變量拷貝更為高效。
  既然volatile關鍵字已經實現了線程間數據同步,又要 synchronized幹什麽呢?呵呵,它們之間有兩點不同。首先,synchronized獲得並釋放監視器——如果兩個線程使用了同一個對象鎖,監 視器能強制保證代碼塊同時只被一個線程所執行——這是眾所周知的事實。但是,synchronized也同步內存:事實上,synchronized在“ 主”內存區域同步整個線程的內存。因此,執行geti3()方法做了如下幾步:
1. 線程請求獲得監視this對象的對象鎖(假設未被鎖,否則線程等待直到鎖釋放)
2. 線程內存的數據被消除,從“主”內存區域中讀入(Java虛擬機能優化此步。。。[後面的不知道怎麽表達,汗])
3. 代碼塊被執行
4. 對於變量的任何改變現在可以安全地寫到“主”內存區域中(不過geti3()方法不會改變變量值)
5. 線程釋放監視this對象的對象鎖
  因此volatile只是在線程內存和“主”內存間同步某個變量的值,而synchronized通過鎖定和解鎖某個監視器同步所有變量的值。顯然synchronized要比volatile消耗更多資源。

更通俗的解釋:

Volatile 字面的意思時易變的,不穩定的。在C#中也差不多可以這樣理解。

編譯器在優化代碼時,可能會把經常用到的代碼存在Cache裏面,然後下一次調用就直接讀取Cache而不是內存,這樣就大大提高了效率。但是問題也隨之而來了。

在多線程程序中,如果把一個變量放入Cache後,又有其他線程改變了變量的值,那麽本線程是無法知道這個變化的。它可能會直接讀Cache裏的數據。但是很不幸,Cache裏的數據已經過期了,讀出來的是不合時宜的臟數據。這時就會出現bug。

用Volatile聲明變量可以解決這個問題。用Volatile聲明的變量就相當於告訴編譯器,我不要把這個變量寫Cache,因為這個變量是可能發生改變的。

C#SpinWait和volatile一點溫習