線程安全
一個程序和方法在面對任何多線程情況下都沒有不確定,那麼就是線程安全的. 線程安全主要通過加鎖和減少線程之間互動的可能性來實現。
通用類型很少全面線程安全的,由於以下幾個原因:
因此,線程安全僅在需要的地方實現。
然而,有一些方法來“欺騙”和有大而復雜的類安全運行在多線程環境下。其中一個是通過封裝大片代碼犧牲粒度--甚至訪問完整的對象--在一個排斥鎖中,強迫在高層串行化訪問它。實際上,這種策略,在使用第三方不安全代碼時是非常關鍵的(大多數框架類型)。這種技巧簡單的使用一個相同的鎖來保護一個不線程安全對象的所有字段,屬性和方法。如果這個對象的方法執行的非常快,那麼這種解決方案工作的非常好(否則,將大量阻塞)。
原始類型之外,很少有框架類是線程安全的,這個保證應該是開發者的責任,通常使用排斥鎖來實現。
另外一種方法是通過最小化共享數據來最少化線程之間的互動。這是一個非常好的方法,常被用在無狀態的中間層或Web服務頁面。因為多個客戶端請求能夠同時到達,因此服務方法必須是線程安全的。一個無狀態的設計(由於可擴展性非常受歡迎)內在限制了互動的可能性,因為類在請求之間不保持數據。線程互動僅在用於創建的靜態字段,用於在內存中緩存常用的數據和提供基本服務,如授權和審計。
最後一種方法是使用原子鎖。.Net框架就是這樣實現的,如果你子類化ContextBoundObject並使用Synchronization屬性到類上。那麼該對象的屬性和方法無論在什麼時候被調用,方法和屬性的執行期間都會采取一個對象的原子鎖。盡管,這減少了線程安全的負擔,但它也帶來了自己的問題:死鎖不發生,可憐的並發性及意外的可重入性。由於這些原因,手動鎖是更好的選擇--至少在一個簡單的自動鎖能被使用之前。
線程安全和.NET框架類型
鎖可以使得非線程安全代碼變得線程安全。.NET框架就是一個好的應用程序:幾乎所有非原始類型在實例化時都不是線程安全的(僅只讀的時候是安全的),然而它們能被用在多線程代碼中,如果通過一個鎖來保護對它的訪問。下面是一個例子,兩個線程同時添加一個Item到同一個List中,然後枚舉這個list:
SafeThreadExample
這個例子中鎖住了list本身。如果有兩個相關的list,那麼不得不選擇在一個通用的對象上加鎖(更好的辦法是使用一個獨立的字段)。
枚舉.NET集合也是線程安全的,如果在枚舉期間list被修改,那麼將拋出一個異常。在這個例子中,拷貝item到一個數組中,而不是在枚舉期間鎖住list。如果枚舉過程非常耗時間,這避免了過分擁有鎖(另外的解決方案是用讀寫鎖)。
圍繞對象加鎖
有時你需要圍繞對象加鎖。想象一下,假設.NET的List是線程安全的,我們想要添加一個item到list:
if(!_list.Contains(newItem))_list.Add(newItem);
不管這個list是否線程安全,語句本身並不是線程安全的。為了防止在測試list是否包含新項和添加新項之間被其它線程搶占,整個if語句必須放在lock中。同樣的鎖也需要放在list被修改的任何地方。如下面的句子也需要放在鎖裡:_list.Clear();為了確保不被搶占。換句話說,我們不得不把它作為一個線程不安全的集合類加鎖(這使得假設list是線程安全是多余的)。
圍繞訪問集合對象加鎖使得在高並發環境中過分阻塞。到目前為止,4.0框架提供了一個線程安全queue,stack和dictionary。
靜態方法
通過一個自定義鎖來封裝對一個對象的訪問,只有在所有並發線程意識到--並且使用--鎖來實現。如果一個對象的socpe很廣,那麼事實並不總是這樣的。最糟的情況是一個public類型含有static成員。想象一下,如果DateTime結構體的靜態字段DateTime.Now,不是線程安全的,兩個並發調用將導致混亂輸出或一個異常。唯一的方法是外部的鎖來鎖住類型的本身--lock(typeof(DateTime))--在調用DateTime.Now之前。如果程序員都同意這樣做,那麼沒有問題(事實這不太可能)。而且,鎖住類型本身也帶來了自己的問題。
由於這個原因,靜態成員必須小心的編程來滿足線程安全。.NET框架中常見的設計是:靜態成員是現存安全的,實例化成員不是線程安全的。根據這個模式當寫訪問public類型時,以便不制造線程安全難題是有意義的。換句話說,通過使靜態函數線程安全,你正在編程以免不妨礙對它的使用。
靜態函數的線程安全並不是它本身的優點而是需要你顯式地來編寫代碼。
只讀線程安全
使得類型對於只讀訪問線程安全(是可能的)是有優勢的,因為它意味著使用它而不必過度加鎖。一些.NET類型遵循了這個原則:如,集合是對於並發讀是線程安全的。
遵循這個原則很簡單:如果你寫一個文檔記錄一個類型對於並發讀是類型安全的,那麼在函數體內不需要寫,使用者會預期它是線程安全的。如,在一個集合的ToArray()函數實現中,你可能通過壓縮內部結構來實現。然而,這將對使用者來說預期只讀是線程安全的。
只讀線程安全是一個枚舉器和可以枚舉分割的一個原因:2個線程能同時枚舉一個集合,因為每一個都有一個獨立的枚舉器對象。
由於文檔缺失,你需要額外小心一個函數是否只讀線程安全。如Random類:當你調用Random.Next()時,它內部實現要求更新私有的種子值。因此,你必須圍繞Random類加鎖,或每個線程一個獨立的對象。
服務器應用程序的線程安全
服務器應用程序需要多個線程來處理多個並發的客戶端。WCF, ASP.NET和Web Services應用程序就是明顯的例子;使用HTTP或TCP的Remoting服務器應用程序也是。這意味著你編寫服務器段代碼,你必須考慮線程安全,如果在處理客戶端請求的線程之間有任何可能的互動。幸運的是,這種可能性很小;一個典型的服務器類是無狀態的(沒有字段)或有一個活動模型為每一個請求創建一個獨立的對象模型。互動經常通過靜態字段來觸發,有時使用緩存來改善性能。
據個例子,假設你有一個RetrieveUser方法去查詢數據庫:
//User is a custom class with fields for user data internal User RetrieveUser(int id){...}
如果這個方法經常被調用,你應該通過緩存數據在一個靜態的Dictionary來改善性能。這是一個考慮了線程安全的解決方案:
static class UserCache { static Dictionary<int,User> _users = new Dictionary<int,User>(); internal static User GetUser(int id) { User u = null; lock(_user) { if(_user.TryGetValue(id, out u)) return u; } u=RetrieveUser(id); ///Method to retrieve from database. lock(_users)_users[id]=u; return u; } }
我們必須最低限度用鎖來讀寫或更新字典確保它的線程安全。這個例子中,我們在性能和簡單之間作了一個折中。我們的設計實際上有非常小的可能潛在低效率:如果2個線程同時調用這個函數並帶有相同的沒有找到的id,那麼RetrieveUser可能被調用2次--字典將被不必要的更新。如果一個鎖跨過整個函數,那麼這種情況將不會發生,但是創建了一個更糟糕的低效:整個cache將在調用RetrieverUser期間被鎖住,其它線程將被阻塞不管你是否查詢其它用戶。
富客戶端和線程相關性
不管是WPF還是Window Form庫都遵循給予線程相關性的模型上。盡管,每一個都有自己獨立的實現,但是他們如何工作卻非常類似。
粉飾富客戶端的對象主要基於WPF的依賴屬性(DependencyObjec)或者Window Form的控件(control)。這些對象有線程相關性,這意味著僅實例化它們的線程才能訪問它們的成員。違反這個原則,將導致不可預料的錯誤或拋出一個異常。
從積極的一面看,你可以不需要加鎖就能訪問一個UI對象。從消極的一面看,你如果想要在Y線程調用對象X的成員,你必須列集(Marshal)這個請求到線程Y。你可以顯式地使用以下方法來這樣做:
Invoke和BeginInvoke都接受一個委托,引用你想運行的目標控件上的方法。Invoke同步工作:調用者阻塞直到函數執行完畢。BeginInvoke異步工作:調用者立即返回,請求將被壓入隊列(使用處理鍵盤,鼠標和定時器相同的消息隊列)。
假設我們有個一個窗體包含了一個文本框叫txtMessage,我們想要從一個工作線程去更新它的內容,下面就是一個WPF的例子:
public partial class MyWindow : Window { public MyWindow() { InitializeComponent(); new Thread(Work).Start(); } void Work() { Thread.Sleep(5000); UpdateMessage("The answer"); } void UpdateMessage(string message) { Action action= ()=>txtMessage.Text=message; Dispatcher.Invokd(action); } }
WF的代碼類似於WPF,除了調用窗體的Invoke來代替。
void UpdateMessage(string message) { Action action= ()=>txtMessage.Text=message; this.Invokd(action); }
框架提供了2方法來簡化這個流程:
工作線程 VS UI線程
想一想一個富客戶端有2個不同類型的線程是非常有幫助的:UI線程和工作線程。UI線程(下面稱“擁有”)實例化UI元素;工作線程沒有。工作線程通常執行很長時間的任務如提取數據。
大多數富客戶端程序只有一個UI線程(也叫主線程)並且偶爾產生工作線程--也直接或使用後台工作線程。這些工作線程為了更新或報告進度列集返回到主線程。
那麼,應用程序什麼時候應該有多個UI線程呢?這主要的場景是當你想要有多個頂層窗體,經常叫SDI(Single Document Interface)程序,如Word。每一個SDI窗體通常顯示本身作為一個獨立的應用程序在任務欄並且大多與其它SDI窗體是孤立的。通過給定一個這樣的擁有UI線程的窗體,應用程序能夠做出更多的響應。
Immutable Objects(不可變得對象)
一個不可變得對象是指它的狀態不能改變--不管是內部還是外部。這個不可變得字段通常聲明為只讀並且在構造時完全初始化。
不可變是一個功能性編程的標記--代替帶有不同屬性的可變對象。LINQ遵循了這個范例。不可變在多線程中也是有價值的,它避免了共享寫的問題--通過消除(或最小化)共享寫。
使用不可不對象的一個原則是封裝一組相關的字段,去最小化加鎖的周期。據一個簡單的例子,假設我們有2個字段:
int _percentComplete;
string _statusMessage;
並且我們想要原子性的讀寫它們。我們可以使用下面的方式而不是圍繞這些字段加鎖:
class ProgressStatus //Represents progress of some activity { public readonly int PercentComplete; public readonly string StatusMessage; // this class might have many more fields... public ProgressStatus(int percentComplete, string statusMessage) { PercentComplete = percentComplete; StatisMessage = statusMessage; } }
然後我們定義一個那種類型的單字段,伴隨鎖對象:readonly object _statusLocker = new object(); ProgressStatus _status;
我們能讀寫那種類型的值而不需要擁有鎖。
注意這是一個不需要用鎖來阻止一組相關屬性不一致的方法。但是這並不能在你使用時阻止它被改變--由於這個原因,你需要一個鎖。在Part5,我們將看到更多的使用不變性來簡化多線程的例子--包括PLINQ。