鎖實現互斥的訪問,用于確保在同一時刻只有一個線程可以進入特殊的代碼片段,考慮下面的類:
class ThreadUnsafe { static int val1, val2; static void Go() { if (val2 != 0) Console.WriteLine (val1 / val2); val2 = 0; }}這不是線程安全的:如果Go方法被兩個線程同時調用,可能會得到在某個線程中除數為零的錯誤,因為val2可能被一個線程設置為零,而另一個線程剛好執行到if和Console.WriteLine語句。
下面用c#中的lock來修正這個問題:
class ThreadSafe { static object locker = new object(); static int val1, val2; static void Go() { lock (locker) { if (val2 != 0) Console.WriteLine (val1 / val2); val2 = 0; } } }在同一時刻只有一個線程可以鎖定同步對象(在這里是locker),任何競爭的的其它線程都將被阻止,直到這個鎖被釋放。如果有大于一個的線程競爭這個鎖,那么他們將形成稱為“就緒隊列”的隊列,以先到先得的方式授權鎖。因為一個線程的訪問不能與另一個重疊,互斥鎖有時被稱之對由鎖所保護的內容強迫串行化訪問。在這個例子中,保護了Go方法的邏輯,以及val1 和val2字段的邏輯。一個等候競爭鎖的線程被阻止將在ThreadState上為WaitSleepJoin狀態。稍后將討論一個線程通過另一個線程調用Interrupt或Abort方法來強制地被釋放。這是用于結束工作線程一個相當高效率的技術。C#的lock 語句實際上是調用Monitor.Enter和Monitor.Exit,中間夾雜try-finally語句的簡略版,下面是實際發生在之前例子中的Go方法:
Monitor.Enter (locker); try { if (val2 != 0) Console.WriteLine (val1 / val2); val2 = 0;}finally { Monitor.Exit (locker); }在同一個對象上,在調用第一個Monitor.Ente之前卻先調用了Monitor.Exit將引發異常。Monitor 也提供了TryEnter方法來實現一個超時功能——也用毫秒或TimeSpan,如果獲得了鎖返回true,反之沒有獲得返回false。TryEnter也可以沒有超時參數,“測試”一下鎖,如果鎖不能被獲取的話就立刻超時。
選擇同步對象
任何對所有有關系的線程都可見的對象都可以作為同步對象,但要滿足一個硬性規定:它必須是引用類型。建議同步對象最好私有在類里面(比如一個私有實例字段)防止無意間從外部鎖定相同的對象。滿足這些規則,則同步對象可以兼對象和保護兩種作用。比如下面List :
class ThreadSafe { List <string> list = new List <string>(); void Test() { lock (list) { list.Add ("Item 1"); ...一個專門字段(如在例子中的locker)是常用的方式 , 因為它可以精確控制鎖的范圍和粒度。用對象或類本身的類型作為一個同步對象,即:
lock (this) { ... }或:
lock (typeof (Widget)) { ... } // 保護訪問靜態的方式是不好的,因為存在可以在公共范圍訪問這些對象的潛在風險。
鎖并沒有以任何方式阻止對同步對象本身的訪問,換言之,x.ToString()不會由于另一個線程調用lock(x) 而被阻止。
嵌套鎖定
線程可以重復鎖定相同的對象,可以通過多次調用Monitor.Enter或lock語句來實現。當對應編號的Monitor.Exit被調用或最外面的lock語句完成后,對象那一刻即被解鎖。這就允許最簡單的語法實現一個方法的鎖調用另一個鎖:
static object x = new object();static void Main() { lock (x) { Console.WriteLine ("I have the lock"); Nest(); Console.WriteLine ("I still have the lock"); } //在這鎖被釋放}static void Nest(){ lock (x) { ... } // 釋放了鎖?沒有完全釋放!}線程只能在最開始的鎖或最外面的鎖時被阻止。
何時進行鎖定
作為一項基本規則,任何和多線程有關的會進行讀和寫的字段都應當加鎖。甚至是極平常的事情——單一字段的賦值操作,都必須考慮到同步問題。在下面的例子中Increment和Assign 都不是線程安全的:
class ThreadUnsafe { static int x; static void Increment() { x++; } static void Assign() { x = 123; }}下面是Increment 和 Assign 線程安全的版本:
class ThreadUnsafe{ static object locker = new object(); static int x; static void Increment() { lock (locker) x++; } static void Assign() { lock (locker) x = 123; }}作為加鎖的另一個選擇,在一些簡單的情況下,也可以使用非阻止同步,將在后面討論即使像這樣的語句需要同步的原因。
鎖和原子操作
如果有很多變量在一些鎖中總是進行讀和寫的操作,那么你可以稱之為原子操作。我們假設x 和 y不停地讀和賦值,他們在鎖內通過locker鎖定:
lock (locker) { if (x != 0) y /= x; }你可以認為x 和 y 通過原子的方式訪問,因為代碼段沒有被其它的線程分開 或 搶占,別的線程改變x 和 y是無效的輸出,你永遠不會得到除數為零的錯誤,保證了x 和 y總是被相同的排他鎖訪問。
性能考量
鎖本身是非常快的,一個鎖在沒有堵塞的情況下一般只需幾十納秒(十億分之一秒)。如果發生堵塞,任務切換帶來的開銷接近于數微秒(百萬分之一秒)的范圍內,盡管在線程重組實際的安排時間之前它可能花費數毫秒(千分之一秒)。相反,該使用鎖而沒使用的會帶來更長的時間開銷。如果發生了死鎖和競爭鎖,鎖就會帶來反作用,由于太多的代碼被放置到鎖語句中了,引起其它線程不必要的被阻止。死鎖是兩線程彼此等待被鎖定的內容,導致兩者都無法繼續下去。爭用鎖是兩個線程任一個都可以鎖定某個內容,如果“錯誤”的線程獲取了鎖,則導致程序錯誤。
對于同步對象非常容易出現死鎖的情況,比較好的處理方式是設計較少的鎖。在一個可信的情況下涉及比較多阻止的話,可以考慮增加鎖的粒度。
線程安全
線程安全的代碼是指在面對任何多線程情況下,代碼都沒有不確定的因素。線程安全首先完成鎖,然后減少在線程間交互的可能性。
一個線程安全的方法,在任何情況下可以可重入式調用。引用類型很少是線程安全的,原因如下:
為了處理一個特定的多線程情況,線程安全經常只在需要實現的地方來實現。不過也有特殊情況,通過犧牲鎖的粒度包含大段的代碼甚至在排他鎖中訪問全局對象來迫使在更高的級別上實現串行化訪問,實現龐大復雜的類安全地運行在多線程環境中。這種用法讓非線程安全的對象用于線程安全代碼中,避免了相同的互斥鎖被用于在保護對非線程安全對象的所有的屬性、方法和字段的訪問上。或者通過最小化共享數據來最小化線程交互,多用于“弱狀態”的中間層程序和web服務器實現引用類型的線程安全。雖然多個客戶端請求同時到達,但每個請求來自它自己的線程(比如asp.net,Web服務器或者遠程體系結構),它們調用的方法是線程安全的。弱狀態設計(因伸縮性好而流行)本質上限制了交互的能力,因此類不能夠在每個請求間持久保留數據。線程交互僅限于可以被選擇創建的靜態字段,一般用于在內存里緩存常用數據和提供認證和審核這樣的基礎設施服務。
線程安全與.NET Framework類型
鎖可用于將非線程安全的代碼轉換成線程安全的代碼。在.NET framework實現中,幾乎所有非基本類型的實例都不是線程安全的。將非基本類型用于多線程代碼中,就需要給訪問的對象進行鎖保護。以下示例中兩個線程同時為相同的List增加條目,然后枚舉它:
class ThreadSafe{ static List <string> list = new List <string>(); static void Main() { new Thread (AddItems).Start(); new Thread (AddItems).Start(); } static void AddItems() { for (int i = 0; i < 100; i++) lock (list)list.Add ("Item " + list.Count); string[] items; lock (list) items = list.ToArray(); foreach (string s in items) Console.WriteLine (s); }}在這種情況下鎖定list對象本身,也許是一個不錯的方式。枚舉.NET的集合也不是線程安全的,在枚舉的時候另一個線程改動list的話,會拋出異常。為了不直接鎖定枚舉過程,我們首先將項目復制到數組中,避免因為在枚舉過程中有潛在的耗時而固定住鎖。
一個有趣的假設:如果List實際上為線程安全的,要增加一個項目到我們假象的線程安全的list里,如下:
if (!myList.Contains (newItem)) myList.Add (newItem);
無論list是否為線程安全的,這個語句顯然不是,也就是說完全線程安全的通用集合類是基本不存在的。.net4.0中,微軟提供了一組線程安全的并行集合類,但他們都經過特殊處理,在訪問方式做了限定。上面的語句要實現線程安全,整個if語句必須放到一個鎖中,用來保護在判斷有無和增加新的之間的搶占。類似的鎖需要用于任何我們需要修改list的地方,比如下面的語句:
myList.Clear();
換言之,我們必須鎖定差不多所有非線程安全的集合類們。內置線程安全,顯而易見是浪費時間!由于這些理由,.NET framework中靜態成員是線程安全的,而一個實例成員則不是。從而在寫自定義類型時,也不要嘗試去創建一個線程安全的自定義組件!當寫公用組件的時候,單獨小心處理靜態成員是一個好的編碼習慣。
新聞熱點
疑難解答