概念
原子操作(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
如上圖中,主存中保存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 直接用緩存的數據進行累加,所以單線程更快。