從此圖中我們會發現 .NET 與C# 的每個版本發布都是有一個“主題”。即:C#1.0托管代碼→C#2.0泛型→C#3.0LINQ→C#4.0動態語言→C#5.0異步編程。現在我為最新版本的“異步編程”主題寫系列分享,期待你的查看及點評。
開始《異步編程:同步基元對象(上)》
如今的應用程序越來越復雜,我們常常需要多線程技術來提高我們應用程序的響應速度。每個線程都由自己的線程ID,當前指令指針(PC),寄存器集合和堆棧組成,但代碼區是共享的,即不同的線程可以執行同樣的函數。所以在並發環境中,多個線程“同時”訪問共享資源時,會造成共享數據損壞,我們可用線程同步鎖來防止。(如果多個線程同時對共享數據只進行只讀訪問是不需要進行同步的)
數據損壞
在並發環境里,當同時對其共享資源進行訪問時可能造成資源損壞,為了避免資源損壞,必須對共享資源進行同步或控制對共享資源的訪問。如果在相同或不同的應用程序域中未能正確地使訪問同步,則會導致出現一些問題,這些問題包括死鎖和爭用條件等:
1) 死鎖:當兩個線程中的每一個線程都在試圖鎖定另外一個線程已鎖定的資源時,就會發生死鎖。其中任何一個線程都不能繼續執行。
2) 爭用條件:兩個或多個線程都可以到達並執行一個代碼塊的條件,根據哪個線程先到達代碼,程序結果會差異很大。如果所有結果都是有效的,則爭用條件是良性的。但是,爭用條件可以與同步錯誤關聯起來,從而導致一個進程干擾另一個進程並可能會引入漏洞。通常爭用條件的可能結果是使程序處於一種不穩定或無效的狀態。
EG:線程T修改資源R后,釋放了它對R的寫訪問權,之后又重新奪回R的讀訪問權再使用它,並以為它的狀態仍然保持在它釋放它之后的狀態。但是在寫訪問權釋放后到重新奪回讀訪問權的這段時間間隔中,可能另一個線程已經修改了R的狀態。
需要同步的資源包括:
1) 系統資源(如通信端口)。
2) 多個進程所共享的資源(如文件句柄)。
3) 由多個線程訪問的單個應用程序域的資源(如全局、靜態和實例字段)。
要鄭重聲明的是:
使一個方法線程安全,並不是說它一定要在內部獲取一個線程同步鎖。一個線程安全的方法意味着在兩個線程試圖同時訪問數據時,數據不會被破壞。比如,System.Math類的一個靜態Max()方法:
public static Int32 Max(Int32 val1,Int32 val2) { return (val1<val2)?val2:val1; }
這個方法是線程安全的,即使它沒有獲取任何鎖。由於Int32是值類型,所以傳給Max的兩個Int32值會復制到方法內部。多個線程可以同時調用Max()方法,每個線程處理的都是它自己的數據,線程之間互不干擾。
線程同步鎖帶來的問題
在並發的環境里,“線程同步鎖”可以保護共享數據,但是也會存在一些問題:
1) 實現比較繁瑣,而且容易錯漏。你必須標識出可能由多個線程訪問的所有共享數據。然后,必須為其獲取和釋放一個線程同步瑣,並且保證已經正確為所有共享資源添加了鎖定代碼。
2) 由於臨界區無法並發運行,進入臨界區就需要等待,加鎖帶來效率的降低。
3) 在復雜的情況下,很容易造成死鎖,並發實體之間無止境的互相等待。
4) 優先級倒置造成實時系統不能正常工作。優先級低的進程拿到高優先級進程需要的鎖,結果是高/低優先級的進程都無法運行,中等優先級的進程可能在狂跑。
5) 當線程池中一個線程被阻塞時,可能造成線程池根據CPU使用情況誤判創建更多的線程以便執行其他任務,然而新創建的線程也可能因請求的共享資源而被阻塞,惡性循環,徒增線程上下文切換的次數,並且降低了程序的伸縮性。(這一點很重要)
什么是原子操作
原子操作是不可分割的,在執行完畢之前不會被任何其它任務或事物中斷。
如何識別原子操作?32位處理器(x86系列)或32位軟件理論上一次能處理32位,也就是4個字節的數據;而64位處理器(x64系列)或64位軟件理論上一次就能處理64位,即8個字節的數據。在處理器|軟件能一次處理的位數范圍內的單個操作即為原子操作。(這段文字也告訴我們:(1)64位操作系統或64位軟件理論上運行更快;(2)32位操作系統上為什么不能運行64位軟件,而反過來卻可以。)
在多線程編程環境中指:一個線程在訪問某個資源的同時能夠保證沒有其他線程會在同一時刻訪問同一資源。.NET為我們提供了多種線程同步的方法,我們可以根據待同步粒度大小來選擇合適的同步方式。
下面介紹下.NET下線程同步的方法。
.NET提供的原子操作
1. 易失結構
volatile 關鍵字指示一個字段可以由多個同時執行的線程修改。JIT編譯器確保對易失字段的所有訪問都是易失讀取和易失寫入的方式執行,而不用顯示調用Thread的靜態VolatileRead()與VolatileWrite()方法。
另外,Volatile關鍵字告訴C#和JIT編譯器不將字段緩存到CPU的寄存器中,確保字段的所有讀取操作都在RAM中進行。(這也會降低一些性能)
volatile 關鍵字可應用於以下類型的字段:
1) 引用類型。
2) 指針類型(在不安全的上下文中)。請注意,雖然指針本身可以是可變的,但是它指向的對象不能是可變的。換句話說,您無法聲明“指向可變對象的指針”。
3) 類型,如 sbyte、byte、short、ushort、int、uint、char、float 和 bool。
4) 具有以下基類型之一的枚舉類型:byte、sbyte、short、ushort、int 或 uint。
5) 已知為引用類型的泛型類型參數。
6) IntPtr 和 UIntPtr。
volatile也帶來了一個問題,因為volatile標注的成員不受優化器優化:
eg:m_amount=m_amount+m_amount // m_amount是類中定義的一個volatile字段
通常,要倍增一個整數,只需將它的所有位都左移1位,許多編譯器都能檢測到上述代碼的意圖,並執行優化。然而,如果m_amount是volatile字段,就不允許執行這個優化,編譯器必須生成代碼將m_amount讀入一個寄存器,再把它讀入另一個寄存器,將兩個寄存器加到一起,再將結果寫回m_amount字段。未優化的代碼肯定會更大,更慢。
另外,C#不支持以傳引用的方式將volatile字段傳給方法。
有時為了利用CPU的寄存器和編譯器的優化我們會采用下面兩種原子操作。
2. 互鎖結構(推薦使用)
互鎖結構是由 Interlocked 類的靜態方法對某個內存位置執行的簡單原子操作,即提供同步對多個線程共享的變量的訪問的方法。這些原子操作包括添加、遞增和遞減、交換、依賴於比較的條件交換、內存屏障,以及 32 位平台上的 64 位long值的讀取操作。
Interlocked的所有方法都建立了完美的內存柵欄。換言之,調用某個Interlocked方法之前的任何變量寫入都在這個Interlocked方法調用之前執行;而這個調用之后的任何變量讀取都在這個調用之后讀取。
詳細情況請看如下API和注釋:
public static class Interlocked { // 對兩個 32|64 位整數進行求和並用和替換第一個整數,上述操作作為一個原子操作完成。返回結果:location1的新值。 public static int Add(ref int location1, int value); public static long Add(ref long location1, long value); // 以原子操作的形式遞增|遞減指定變量的值。返回結果:location1的新值。 public static int Increment(ref int location); public static long Increment(ref long location); public static int Decrement(ref int location); public static long Decrement(ref long location); // 比較指定的location1和comparand是否相等,如果相等,則將location1值設置為value。返回結果:location1 的原始值。 public static T CompareExchange<T>(ref T location1, T value, T comparand) where T : class; // 以原子操作的形式,將location1的值設置為value,返回結果:location1 的原始值。 public static T Exchange<T>(ref T location1, T value) where T : class; // 按如下方式同步內存存取:執行當前線程的處理器在對指令重新排序時,不能采用先執行 Interlocked.MemoryBarrier() // 調用之后的內存存取,再執行 Interlocked.MemoryBarrier() 調用之前的內存存取的方式。 /// 此方法在.NET Framework 4.5 中引入,它是 Thread.MemoryBarrier() 方法的包裝。 public static void MemoryBarrier(); // 返回一個以原子操作形式加載的 64 位值。location:要加載的 64 位值。 public static long Read(ref long location); …… }
注意:
1) 在使用Add()、Increament()、Decrement()方法時可能出現溢出情況,則遵循規則:
a) 如果 location=Int32.MaxValue,則 location+1 =Int32.MinValue,location+2=Int32.MinValue+1……。
b) 如果 location=Int32.MinValue,則 location- 1 =Int32.MaxValue,location- 2 =Int32.MaxValue-1……
2) Read(ref long location) 返回一個以原子操作形式加載的 64 位值。由於 64 位讀取操作已經是原子的,因此 64 位系統上不需要 Read 方法。在 32 位系統上,64 位讀取操作除非用 Read 執行,否則不是原子的。
3) Exchange 和 CompareExchange 方法具有接受 object 類型的參數的重載。這重載的第一個參數都是 ref object,傳遞給此參數的變量嚴格類型化為object,不能在調用這些方法時簡單地將第一個參數強制轉換為object類型,否則報錯“ref 或 out 參數必須是可賦值的變量”
這實際是類型強制轉換的一個細節,強制轉換時編譯器會生成一個臨時引用,然后把這個臨時引用傳給一個和轉換類型相同的引用,這個臨時引用比較特別,它不能被賦值,所以會報“ref 或 out 參數必須是可賦值的變量”。
比如:
int o=2; // 編譯報錯“ref 或 out 參數必須是可賦值的變量” Interlocked.Exchange(ref (object)o,new object()); // 編譯通過 objectobj = (object)o; Interlocked.Exchange(ref obj, new object());
4) 示例:
在大多數計算機上,增加變量操作不是一個原子操作,需要執行下列步驟:
a) 將實例變量中的值加載到寄存器中。
b) 增加或減少該值。
c) 在實例變量中存儲該值。
如果不使用 Increment 和 Decrement,線程可能會在執行完前兩個步驟后被搶先。然后由另一個線程執行所有三個步驟。當第一個線程重新開始執行時,它改寫實例變量中的值,造成第二個線程執行增減操作的結果丟失。(線程都維護着自己的寄存器)
3. Thread類為我們提供的VolatileRead()與VolatileWrite()靜態方法。請參見《異步編程:線程概述及使用》
同步代碼塊(臨界區)
1. Monitor(監視器)
Monitor(監視器)放置多個線程同時執行代碼塊。Enter 方法允許一個且僅一個線程繼續執行后面的語句;其他所有線程都將被阻止,直到執行語句的線程調用 Exit。
Monitor 鎖定對象是引用類型,而非值類型,該對象用來定義鎖的范圍。盡管可以向 Enter 和 Exit 傳遞值類型,但對於每次調用它都是分別裝箱的。因為每次調用都創建一個獨立的對象(即,鎖定的對象不一樣),所以 Enter要保護的代碼並沒有真正同步。另外,傳遞給 Exit 的被裝箱對象不同於傳遞給 Enter 的被裝箱的對象,所以 Monitor 將引發 SynchronizationLockException,並顯示以下消息:“從不同步的代碼塊中調用了對象同步方法。”
Monitor將為每個同步對象來維護以下信息:
1) 對當前持有鎖的線程的引用。
2) 對就緒隊列的引用。當一個線程嘗試着lock一個同步對象的時候,該線程就在就緒隊列中排隊。一旦沒人擁有該同步對象,就緒隊列中的線程就可以占有該同步對象。(隊列:先進先出)
3) 對等待隊列的引用。占有同步對象的線程可以暫時通過Wait()釋放對象鎖,將其在等待隊列中排隊。該隊列中的線程必須通過Pulse()\PulseAll()方法通知才能進入到就緒隊列。(隊列:先進先出)
Monitor靜態類
public static class Monitor { // 確定當前線程是否保留指定對象鎖。 public static bool IsEntered(object obj); // 獲取指定對象上的排他鎖(設置獲取鎖的結果lockTaken,通過引用傳遞。 輸入必須為 false。 如果已獲取鎖,則輸出為 true;否則輸出為 false) public static void Enter(object obj); public static void Enter(object obj, ref bool lockTaken); // 在指定的一段時間內,嘗試獲取指定對象上的排他鎖. // (設置獲取鎖的結果lockTaken,通過引用傳遞。 輸入必須為 false。如果已獲取鎖,則輸出為 true;否則輸出為 false) // System.TimeSpan,表示等待鎖所需的時間量。 值為 -1 毫秒表示指定無限期等待。 public static bool TryEnter(object obj, TimeSpan timeout); public static void TryEnter(object obj, TimeSpan timeout, ref bool lockTaken); // 釋放指定對象上的排他鎖。 public static void Exit(object obj); // 釋放對象上的鎖並阻止當前線程,直到它重新獲取該鎖。 System.TimeSpan,表示線程進入就緒隊列之前等待的時間量。 // exitContext標識可以在等待之前退出同步上下文的同步域,隨后重新獲取該域。 public static bool Wait(object obj, TimeSpan timeout, bool exitContext); // 通知等待隊列中的線程鎖定對象狀態的更改。 public static void Pulse(object obj); // 通知所有的等待線程對象狀態的更改。 public static void PulseAll(object obj); …… }
分析:
1) 同一線程在不阻止的情況下允許多次調用 Enter();但在該對象上等待的其他線程取消阻止之前必須調用相同數目的 Exit()。
2) 如果釋放了鎖並且其他線程處於該對象的【就緒隊列】中,則其中一個線程將獲取該鎖。如果其他線程處於【等待隊列】中,則它們不會在鎖的所有者調用 Exit ()時自動移動到就緒隊列中。
3) 喚醒機制:Wait()釋放參數指定對象的對象鎖,以便允許其他被阻塞的線程獲取對象鎖。調用Wait()的線程進入【等待隊列】中,等待狀態必須由其他線程調用方法Pulse()或PulseAll()喚醒,使等待狀態線程變為就緒狀態。
方法Pulse()和PulseAll():向【等待隊列】中第一個或所有等待線程發送信息,占用對象鎖的線程准備釋放對象鎖。在即將調用Exit()方法前調用,通知等待隊列線程移入就緒隊列,待執行方法Exit()釋放對象鎖后被Wait()的線程將重新獲取對象鎖。
2. lock
lock 是.NET為簡化Monitor(監視器)而存在的關鍵字。其行為等價於:
Boolean lockTaken=false; try { Mnoitor.Enter(鎖定對象,ref lockTaken); …… } Finally { if(lockTaken) Monitor.Exit(鎖定對象); }
盡管lock使用起來比Monitor對象更加簡潔,然而Monitor類還提供了其他的方法,通過這些方法可以對獲得鎖的過程有更多的控制,而且可以使用超超時。
3. 流程圖
認識了Monitor和lock后,我們再看下內部獲得獨占鎖的流程圖,能讓我們有更好的理解:
1. 示例,雙檢鎖(Double-Check Locking)
雙檢鎖(Double-Check Locking),開發人員用它將一個單實例對象的構造推遲到一個應用程序首次請求這個對象的時候進行。這有時也稱為延遲初始化(lazy initialization)。
public sealed class Singleton { private static Object s_lock = new object(); private static Singleton s_value = null; // 私有構造器,阻止這個類外部的任何代碼創建實例 private Singleton() { } public static Singleton GetSingleton() { if (s_value != null) return s_value; Monitor.Enter(s_lock); if (s_value == null) { s_value = new Singleton(); // Singleton temp = new Singleton(); // Interlocked.Exchange(ref s_value, temp); } Monitor.Exit(s_lock); return s_value; } }
分析:
1) 里面有兩個if,當第一個if判斷存在對象時就快速返回,就不需線程同步。如果第一個if判斷對象還沒創建好,就會獲取一個線程同步鎖來確保只有一個線程構造單實例函數。
2) 細膩的你可能認為會出現一種情況:第一個if將s_value空值讀入到一個CPU寄存器中,而到第二個if讀取s_value時也是從寄存器中讀取該空值,但此時s_value內存中的值可能已經不為空了。
CLR已經幫我們解決了這個問題,在CLR中任何鎖的調用構成了一個完整的內存柵欄,在柵欄之前寫入的任何變量都必須在柵欄之前完成;在柵欄之后的任何變量都必須在柵欄之后開始。即此處的Monitor.Enter()使s_value之前寄存器中的緩存無效化,需重新從內存中讀取。
3) Interlocked.Exchange()方法的調用。若不使用此方法可能出現:為Singleton分配內存,將引用賦給s_value,再調用構造器。在調用構造器之前另一個線程訪問第一個if語句,並返回了一個構造器還沒有執行完畢的實例。
這個結論是錯誤的,驗證思路:(感謝園友 JustForKim 指出問題)
a) 本想試着借用ildasm工具反編譯出IL代碼進行查看,結果……看不懂
b) 采取第二種方式:跑兩個線程,一個線程創建實例,並在構造函數中加入耗時操作Thread.Spin(Int32.MaxValue),另一個線程不斷訪問s_value。得出結果是執行完構造函數后才會將變量引用返回。代碼如下:
class Program { private static Singleton s_value = null; static void Main(string[] args) { ThreadPool.QueueUserWorkItem((obj) => { Singleton.GetSingleton(); }); ThreadPool.QueueUserWorkItem((obj) => { while (true) { if (s_value != null) { Console.WriteLine("s_lock不為null"); break; } } }); Console.Read(); } public sealed class Singleton { private static Object s_lock = new object(); // 私有構造器,阻止這個類外部的任何代碼創建實例 private Singleton() { Thread.SpinWait(Int32.MaxValue); Console.WriteLine("對象創建完成"); } public static Singleton GetSingleton() { if (s_value != null) return s_value; Monitor.Enter(s_lock); if (s_value == null) { s_value = new Singleton(); //Singleton temp = new Singleton(); //Interlocked.Exchange(ref s_value, temp); Console.WriteLine("賦值完成"); } Monitor.Exit(s_lock); return s_value; } } }
輸出結果:
4) 代碼中沒有使用try-catch-finally確保鎖總是得以釋放。原因: (所以我們要避免使用lock關鍵字)
a) 在try塊中,如果在更改狀態的時候發生了一個異常,這個狀態處於損壞狀態。鎖在finally塊中退出時,另一個線程可能操作損壞的狀態。
b) 進入和離開try塊也會影響方法的性能。
使用Win32對象同步:互斥體、事件與信號量
1. WaitHandle抽象類
System.Threading.WaitHandle抽象基類提供了三個繼承類,如圖所示:
等待句柄提供了豐富的等待和通知功能。等待句柄派生自 WaitHandle 類,WaitHandle 類又派生自 MarshalByRefObject。因此,等待句柄可用於跨應用程序域邊界同步線程的活動。
1) 字段:
public const int WaitTimeout WaitAny返回滿足等待的對象的數組索引;如果沒有任何對象滿足等待,並且WaitAny()設置的等待的時間間隔已過,則返回WaitTimeout。
2) 屬性:
Handle,SafeWaitHandle 獲取或設置一個Win32內核對象的句柄,該句柄在構造一個WaitHandle派生類時初始化。
a) Handle已過時,給 Handle 屬性賦新值不會關閉上一個句柄。這可能導致句柄泄漏。
b) SafeWaitHandle,代替Handle,給 SafeWaitHandle 屬性賦新值將關閉上一個句柄。
3) Close()和Dispose()
使用Close()方法釋放由 WaitHandle 的實例持有的所有資源。Close()釋放后不會像DbConnection對象一樣還可打開,所以通常在對象使用完后直接通過IDisposable.Dispose() 方法釋放對象。
4) SignalAndWait(),WaitAll(),WaitAny(),WaitOne()
共同參數:
等待的間隔 |
如果值是 System.Threading.Timeout.Infinite,即 -1,則等待是無限期的。 |
是否退出上下文的同步域 |
如果等待之前先退出上下文的同步域(如果在同步上下文中),並在稍后重新獲取它,則為 true;即線程在等待時退出上下文同步域並釋放資源,這樣該同步域被阻塞的線程才能獲取鎖定資源。當等待方法返回時,執行調用的線程必須等待重新進入同步域。 SignalAndWait()、WaitOne()默認傳false。 WaitAll()、WaitAny()默認傳true。 |
WaitOne()基於WaitSingleObject,WaitAny() 或 WaitAll()基於WaitmultipleObject。WaitmultipleObject實現要比WaitSingleObject復雜的多,性能也不好,盡量少用。
a) SignalAndWait (WaitHandle toSignal, WaitHandle toWaitOn)
向 toSignal 發出信號並等待toWaitOn。如果信號和等待都成功完成,則為 true;如果等待沒有完成,則此方法不返回。這樣toSignal所在線程結束前必須調用toWaitOn.Set()或和別的線程協作由別的線程調用toWaitOn.Set(),SignalAndWait()才不阻塞調用線程。
b) WaitAll()
接收WaitHandle對象數組作為參數,等待該數組中的所有WaitHandle對象都收到信號。在具有 STAThreadAttribute 的線程中不支持 WaitAll ()方法。
c) WaitAny()
接收WaitHandle對象數組作為參數,等待該數組中的任意WaitHandle對象都收到信號。返回值:滿足等待的對象的數組索引;如果沒有任何對象滿足等待,並且WaitAny()設置的等待的時間間隔已過,則為返回WaitTimeout。
d) WaitOne()
阻塞當前線程,直到當前的 WaitHandle 收到信號
e) 注意一個限制:
在傳給WaitAny()和WaitAll()方法的數組中,包含的元素不能超過64個,否則方法會拋出一個System.NotSupportedException。
2. 事件等待句柄--- EventWaitHandle、AutoResetEvent、ManualResetEvent
事件等待句柄(簡稱事件)就是可以通過發出相應的信號來釋放一個或多個等待線程的等待句柄。
事件等待句柄通常比使用 Monitor.Wait() 和 Monitor.Pulse(Object) 方法更簡單,並且可以對信號發送提供更多控制。命名事件等待句柄也可用於跨應用程序域和進程同步活動,而監視器Monitor只能用於本地的應用程序域。
1) EventWaitHandle
EventWaitHandle 類允許線程通過發出信號和等待信號來互相通信。信號發出后,可以用手動或自動方式重置事件等待句柄。 EventWaitHandle 類既可以表示本地事件等待句柄(本地事件),也可以表示命名系統事件等待句柄(命名事件或系統事件,對所有進程可見)。
public class EventWaitHandle : WaitHandle { public EventWaitHandle(bool initialState, EventResetMode mode, string name , out bool createdNew, EventWaitHandleSecurity eventSecurity); // 獲取 System.Security.AccessControl.EventWaitHandleSecurity 對象, // 該對象表示由當前 EventWaitHandle 對象表示的已命名系統事件的訪問控制安全性。 public EventWaitHandleSecurity GetAccessControl(); // 設置已命名的系統事件的訪問控制安全性。 public void SetAccessControl(EventWaitHandleSecurity eventSecurity); // 打開指定名稱為同步事件(如果已經存在)。 public static EventWaitHandle OpenExisting(string name); // 用安全訪問權限打開指定名稱為同步事件(如果已經存在)。 public static EventWaitHandle OpenExisting(string name, EventWaitHandleRights rights); public static bool TryOpenExisting(string name, out EventWaitHandle result); public static bool TryOpenExisting(string name, EventWaitHandleRights rights, out EventWaitHandle result); // 將事件狀態設置為非終止狀態,導致線程阻止。 public bool Reset(); // 將事件狀態設置為終止狀態,允許一個或多個等待線程繼續。 public bool Set(); …… }
i. 構造函數
initialState |
如果為 true,EventWaitHandle為有信號狀態,此時不阻塞線程。 |
EventResetMode |
指示在接收信號后是自動重置 EventWaitHandle 還是手動重置。 枚舉值: public enum EventResetMode { AutoReset = 0, ManualReset = 1, } |
createdNew |
在此方法返回時,如果創建了本地事件(如果 name 為空字符串)或指定的命名系統事件,則為 true;如果指定的命名系統事件已存在,則為 false。可以創建多個表示同一系統事件的 EventWaitHandle 對象。 |
eventSecurity |
一個 EventWaitHandleSecurity 對象,表示應用於【已命名的系統事件】的訪問控制安全性。如果系統事件不存在,則使用指定的訪問控制安全性創建它。如果該事件存在,則忽略指定的訪問控制安全性。 |
ii. OpenExisting中使用的EventWaitHandleRights枚舉
// 指定可應用於命名的系統事件對象的訪問控制權限。 [Flags] public enum EventWaitHandleRights { // set()或reset()命名的事件的信號發送狀態的權限。 Modify = 2, // 刪除命名的事件的權限。 Delete = 65536, // 打開並復制某個命名的事件的訪問規則和審核規則的權限。 ReadPermissions = 131072, // 更改與命名的事件關聯的安全和審核規則的權限。 ChangePermissions = 262144, // 更改命名的事件的所有者的權限。 TakeOwnership = 524288, // 在命名的事件上等待的權限。 Synchronize = 1048576, // 對某個命名的事件進行完全控制和修改其訪問規則和審核規則的權限。 FullControl = 2031619, }
默認設置為EventWaitHandleRights.Synchronize | EventWaitHandleRights.Modify。如果你顯示為其設置權限,也必須給予這兩個權限。
2) AutoResetEvent類(本地事件)
AutoResetEvent用於表示自動重置的本地事件。在功能上等效於用EventResetMode.AutoReset 創建的本地EventWaitHandle。
public sealed class AutoResetEvent : EventWaitHandle { public AutoResetEvent(bool initialState); }
使用方式:調用 Set() 向 AutoResetEvent 發信號以釋放等待線程。AutoResetEvent 將保持終止狀態,直到一個正在等待的線程被釋放,然后自動重置為非終止狀態。如果沒有任何線程在等待,則狀態將無限期地保持為終止狀態,直到一個線程進入就緒隊列,此時線程會立馬被釋放繼續執行,等待句柄也會被設置為非終止狀態從而等待下一次Set()。
3) ManualResetEvent類(本地事件)
ManualResetEvent表示必須手動重置的本地事件。在功能上等效於用EventResetMode.ManualReset 創建的本地 EventWaitHandle。
public sealed class ManualResetEvent : EventWaitHandle { public ManualResetEvent(bool initialState); }
使用方式:調用 Set()向ManualResetEvent發信號以釋放等待線程。ManualResetEvent將一直保持終止狀態,直到它主動調用 Reset ()方法或直到釋放完等待句柄中的所有線程(即所有WaitOne()都獲得信號)。
4) Mutex(互斥體)
Mutex 是同步基元,它只向一個線程授予對共享資源的獨占訪問權。
Mutex的API與EventWaitHandleAPI類似。
public sealed class Mutex : WaitHandle { // 使用一個指示調用線程是否應擁有互斥體的初始所屬權的布爾值、一個作為互斥體名稱的字符串, // 以及一個在方法返回時指示調用線程是否被授予互斥體的初始所屬權的布爾值來初始化 Mutex 類的新實例。 public Mutex(bool initiallyOwned, string name, out bool createdNew); // 釋放 System.Threading.Mutex 一次。 public void ReleaseMutex(); …… }
構造器參數:
initiallyOwned 如果為 true,則給予調用線程已命名的系統互斥體的初始所屬權;否則為 false。
如果 name 不為空字符串且 initiallyOwned 為 true,則只有當參數 createdNew 在調用后為 true 時,調用線程才擁有已命名的互斥體。否則,此線程可通過調用 WaitOne() 方法來請求互斥體。
使用方式:可以使用Mutex.WaitOne() 方法請求互斥體的所屬權。擁有互斥體的線程可以在對 WaitOne()的重復調用中請求相同的互斥體而不會阻止其執行。但線程必須調用 ReleaseMutex() 方法同樣多的次數以釋放互斥體的所屬權。(工作方式類似Monitor監視器)
Mutex 類比 Monitor 類使用更多系統資源,但是它可以使用命名互斥體跨應用程序域邊界進行封送處理,可用於多個等待(WaitAny()/WaitAll()),並且可用於同步不同進程中的線程。
在運行終端服務的服務器上,已命名的“系統 mutex”可以具有兩級可見性。
a) 如果名稱以前綴“Global\”開頭,則 mutex 在所有終端服務器會話中均為可見。
b) 如果名稱以前綴“Local\”開頭,則 mutex 僅在創建它的終端服務器會話中可見。在這種情況下,服務器上各個其他終端服務器會話中都可以擁有一個名稱相同的獨立 mutex。如果創建已命名 mutex 時不指定前綴,則默認將采用前綴“Local\”。
異常:如果線程終止而未釋放 Mutex,則認為該 mutex 已放棄。這是嚴重的編程錯誤,因為該 mutex 正在保護的資源可能會處於不一致的狀態,獲取該 mutex 的下一個線程中將引發 AbandonedMutexException。
5) Semaphore(信號量)
限制可同時訪問某一資源或資源池的線程數。
Semaphore的API與EventWaitHandleAPI類似。
public sealed class Semaphore : WaitHandle { // 初始化 Semaphore 類的新實例,並指定最大並發入口數及初始請求數,以及選擇指定系統信號量對象的名稱。 public Semaphore(int initialCount, int maximumCount, string name); // 退出信號量並返回調用 Semaphore.Release 方法前信號量的計數。 public int Release(); // 以指定的次數退出信號量並返回調用 Semaphore.Release 方法前信號量的計數。 public int Release(int releaseCount); …… }
使用方式:信號量的計數在每次線程進入信號量時減小(eg:WaitOne()),在線程釋放信號量時增加(eg:Release())。當計數為零時,后面的請求將被阻塞,直到有其他線程釋放信號量。
WaitHandle的派生類具有不同的線程關聯
1. Mutex具有線程關聯。擁有Mutex 的線程必須將其釋放,而如果在不擁有mutex的線程上調用ReleaseMutex方法,則將引發異常ApplicationException。
2. 事件等待句柄(EventWaitHandle、AutoResetEvent 和 ManualResetEvent)以及信號量(Semaphore)沒有線程關聯。任何線程都可以發送事件等待句柄或信號量的信號。
命名事件
Windows 操作系統允許事件等待句柄具有名稱。命名事件是系統范圍的事件。即,創建命名事件后,它對所有進程中的所有線程都是可見的。因此,命名事件可用於同步進程的活動以及線程的活動。系統范圍的,可以用來協調跨進程邊界的資源使用。
注意:
1) 因為命名事件是系統范圍的事件,所以可以有多個表示相同命名事件的 EventWaitHandle 對象。每當調用構造函數或 OpenExisting 方法時時,都會創建一個新的 EventWaitHandle 對象。重復指定相同名稱會創建多個表示相同命名事件的對象。
2) 使用命名事件時要小心。因為它們是系統范圍的事件,所以使用同一名稱的其他進程可能會意外地阻止您的線程。在同一計算機上執行的惡意代碼可能以此作為一個切入點來發動拒絕服務攻擊。
應使用訪問控制安全機制來保護表示命名事件的 EventWaitHandle 對象
a) 最好通過使用可指定 EventWaitHandleSecurity 對象的構造函數來實施保護。
b) 也可以使用 SetAccessControl 方法來應用訪問控制安全,但這一做法會在事件等待句柄的創建時間和設置保護時間之間留出一段漏洞時間。
使用訪問控制安全機制來保護事件可幫助阻止惡意攻擊,但無法解決意外發生的名稱沖突問題。
3) Mutex、Semaphore對象類似EventWaitHandle。(AutoResetEvent 和 ManualResetEvent 只能表示本地等待句柄,不能表示命名系統事件。)
利用特性進行上下文同步和方法同步
1. SynchronizationAttribute(AttributeTargets.Class)
應用SynchronizaitonAttribute的類,CLR會自動對這個類實施同步機制。為當前上下文和所有共享同一實例的上下文強制一個同步域(同步域之所以有意義就在於它不能被多個線程所共享。換句話說,一個處在同步域中的對象的方法是不能被多個線程同時執行的。這也意味着在任一時刻,最多只有一個線程處於同步域中)。
被應用SynchronizationAttribute的類必須是上下文綁定的。換句話說,它必須繼承於System.ContextBoundObject類。
一般類所建立的對象為上下文靈活對象(context-agile),它們都由CLR自動管理,可存在於任意的上下文當中(一般在默認上下文中)。而 ContextBoundObject 的子類所建立的對象只能在建立它的對應上下文中正常運行,此狀態被稱為上下文綁定。其他對象想要訪問ContextBoundObject 的子類對象時,都只能通過代透明理來操作。
示例:
using System.Runtime.Remoting.Contexts; class Synchronization_Test { public static void Test() { class1 c = new class1(); ThreadPool.QueueUserWorkItem(o => { c.Test1(); }); Thread.Sleep(100); ThreadPool.QueueUserWorkItem(o => { c.Test2(); }); } [Synchronization(SynchronizationAttribute.REQUIRED)] internal class class1 : ContextBoundObject {// 必須繼承於System.ContextBoundObject類 public void Test1() { Thread.Sleep(1000); Console.WriteLine("Test1"); Console.WriteLine("1秒后"); } public void Test2() { Console.WriteLine("Test2"); } } } /* 輸出: Test1 1秒后 Test2 /*
SynchronizationAttribute 類對那些沒有手動處理同步問題經驗的開發人員來說是很有用的,因為它囊括了特性所標注的類的實例變量,實例方法以及實例字段。它不處理靜態字段和靜態方法的同步。 (SynchronizationAttribute鎖的吞吐量低,一般不使用)
除此之外,還有另一個SynchronizationAttribute。System.EnterpriseServices. SynchronizationAttribute擁有同樣的目的只不過在內部使用了COM+中用於同步的企業服務。
基於以下原因,我們優先選擇使用System.Runtime.Remoting.Contexts.SynchronizationAttribute:
1) 它的使用更加高效。
2) 相較於COM+的版本,該機制支持異步調用。
2. MethodImplAttribute(AttributeTargets.Constructor | AttributeTargets.Method)
如果臨界區跨越整個方法,則可以通過將 System.Runtime.CompilerServices.MethodImplAttribute 放置在方法上,並指定MethodImplOptions.Synchronized參數,可以確保在不同線程中運行的該方法以同步的方式運行。
a) MethodImplAttribute應用到instance method相當於lock(this),鎖定該類實例。所以它們和不使用此特性,直接使用lock(this)的方法互斥。
b) MethodImplAttribute應用到static method相當於lock (typeof (該類))。所以它們和不使用此特性,直接使用lock (typeof (該類))的方法互斥。
該屬性將使當前線程持有鎖,直到方法返回;如果可以更早釋放鎖,則使用 Monitor 類或 lock 語句而不是該屬性。
驗證示例:
internal class class1 { [MethodImpl(MethodImplOptions.Synchronized)] public static void Static_Test1() { Thread.Sleep(1000); Console.WriteLine("MethodImpl特性標注的靜態方法----1"); Console.WriteLine("1秒后釋放lock (typeof(class1))"); } public static void Static_Test2() { // MethodImplAttribute應用到static method相當於lock (typeof (該類))。 lock (typeof(class1)) { Console.WriteLine("MethodImpl特性標注的靜態方法----2"); } } public static void Static_Test3() { Console.WriteLine("MethodImpl特性標注的靜態方法----3"); } } // 調用: ThreadPool.QueueUserWorkItem(o => { class1.Static_Test1(); }); Thread.Sleep(100); ThreadPool.QueueUserWorkItem(o => { class1.Static_Test2(); }); ThreadPool.QueueUserWorkItem(o => { class1.Static_Test3(); }); /* 輸出: MethodImpl特性標注的靜態方法----3 MethodImpl特性標注的靜態方法----1 1秒后釋放lock (typeof(class1)) MethodImpl特性標注的靜態方法----2 */
集合類的同步
.NET在一些集合類,比如Queue、ArrayList、HashTable和Stack,已經提供了Synchronized ()方法和SyncRoot屬性。
1. Synchronized()原理是返回了一個線程安全的對象,比如Hashtable.Synchronized(new Hashtable())返回了一個繼承自Hashtable類的SyncHashtable對象,該對象在沖突操作上進行了lock(SyncRoot屬性)從而確保了線程同步。
2. SyncRoot屬性提供了一個專門待鎖定對象,如Hashtable中實現源碼:
public virtual object SyncRoot { get { if(this._syncRoot==null) { Interlocked.CompareExchange(ref this._syncRoot, new object(), null); } return this._syncRoot; } }
從源碼可知,SyncRoot實際上就是通過Interlocked返回一個同步的object類型對象。
注意:此處的SyncRoot模式並不推薦使用,因為至始至終都應使用私有的鎖;推薦在自己的類中實現私有的SyncRoot模式並使用。
本博文介紹了死鎖,爭用條件,線程同步鎖帶來的問題,原子操作,volatile\Interlocker\Monitor\WaitHandle\Mutex\EventWaitHandle\AutoResetEvent\ManualResetEvent\Semaphore,SynchronizationAttribute\MethodImplAttribute……
接下來將介紹.NET4.0新增加的混合線程同步基元,篇幅較長所以分為上、下兩篇。在下篇將介紹.NET4.0增加的新混合線程同步基元,這些新基元在一些場合下為我們提供了更好的性能,之所以性能好是因為用戶基元模式與內核基元模式的性能差別,敬請觀看下文。
本節到此結束,感謝大家的觀賞。贊的話還請多推薦啊 (*^_^*)----預祝各位“元旦快樂”
推薦閱讀:
《理論與實踐中的 C# 內存模型》
參考資料:
《CLR via C#(第三版)》