標籤:源碼 監視 types float 一段 拷貝 監視器 show 優先順序
今天看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代碼:
- int i1; int geti1() {return i1;}
- volatile int i2; int geti2() {return i2;}
- 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一點溫習