一、CLR線程池基礎
前面說過,創建和銷毀線程是一個比較昂貴的操作,太多的線程也會浪費內存資源。由于操作系統必須調度可運行的線程并執行上下文切換,所以太多的線程還有損于性能。為了改善這個情況,CLR使用了代碼來管理它自己的線程池。可將線程池想像成可由你的應用程序使用的一個線程集合。每個進程都有一個線程池,它在各個應用程序域(AppDomain)是共享的.
CLR初始化時,線程池是沒有線程的。在內部,線程池維護了一個操作請求隊列。應用程序想執行一個異步操作時,就調用某個方法,將一個記錄項(entry)追加到線程池的隊列中。線程池的代碼從這個隊列中提取記錄項,將這個記錄項派遣(dispatch)給一個線程池線程。如果線程池中沒有線程,就創建新的線程。創建線程要產生一定的性能損失。然而,當線程池完成任務后,線程不會被銷毀。相反,線程會返回線程池,在那里進入空閑狀態,等待響應另一個請求。由于線程不銷毀自身,所以不再產生額外的性能損失。 如果你的應用程序向線程池發出許多請求,線程池會嘗試只用一個線程來服務所有的請求。然而,如果你的應用程序發出請求的速度超過了線程池處理它們的速度,就會創建額外的線程。最終,你的應用程序所有請求都可能有少量的線程處理,所有線程池不必創建大量的線程。 如果你的應用程序停止向線程池發出請求,池中含有大量空閑的線程。這是對內存資源的一種浪費。所以,當一個線程池線程空閑一段時間以后,線程會自己醒來終止自己以釋放資源。 線程終止自己時,會產生一定的性能損失。然后,線程終止自己的情況下,表明你的應用程序本身就沒有做什么事情,所以這個性能損失關系不大。 在內部,線程池將自己的線程劃分為工作者(Worker)線程和I/O線程。應用程序要求線程池執行一個異步的計算限制操作時(這個操作可能發起一個I/O限制的操作),使用的就是工作者線程。I/O線程用于通知你的代碼一個異步I/O限制操作已經完成,具體的說,這意味著使用"異步編程模型"發出I/O請求,比如訪問文件、網絡服務器、數據庫等等。 二、執行簡單的計算限制操作 將一個異步的、計算限制的操作放到一個線程池的隊列中,通常可以調用ThreadPool類定義的以下方法之一://將方法排入隊列以便執行。此方法在有線程池線程變得可用時執行。static Boolean QueueUserWorkItem(WaitCallback callBack);//將方法排入隊列以便執行,并指定包含該方法所用數據的對象。此方法在有線程池線程變得可用時執行。static Boolean QueueUserWorkItem(WaitCallback callBack,Object state);這些方法向線程池的隊列中添加一個"工作項"(work item)以及可選的狀態數據,如果此方法成功排隊,則為 true;如果無法將該工作項排隊,則引發 OutOfMemoryException。工作項其實就是由callBack參數標識的一個方法,該方法將由線程池線程調用。可通過state實參(狀態數據)向方法傳遞一個參數。無state參數的那個版本的QueueUserWorkItem則向回調方法傳遞null。最終,池中的某個線程會處理工作項,造成你指定的方法被調用。你寫的回調方法必須匹配System.Threading.WaitCallBack委托類型,它的定義如下:
delegate void WaitCallback(Object state);以下演示了如何讓一個線程池線程以異步方式調用一個方法:
class PRogram { static void Main(string[] args) { Console.WriteLine("Main thread: queuing an asynchronous Operation"); ThreadPool.QueueUserWorkItem(ComputeBoundOp, 5); Console.WriteLine("Main thread: Doing other work here..."); Thread.Sleep(10000); // 模擬其它工作 (10 秒鐘) //Console.ReadLine(); } // 這是一個回調方法,必須和WaitCallBack委托簽名一致 private static void ComputeBoundOp(Object state) { // 這個方法通過線程池中線程執行 Console.WriteLine("In ComputeBoundOp: state={0}", state); Thread.Sleep(1000); // 模擬其它工作 (1 秒鐘) // 這個方法返回后,線程回到線程池,等待其他任務 } }我編譯運行的結果是:Main thread: queuing an asynchronous operationMain thread: Doing other work here...In ComputeBoundOp: state=5 但有時也會得到一下輸出:Main thread: queuing an asynchronous operationIn ComputeBoundOp: state=5Main thread: Doing other work here... 之所以有兩種輸出結果,是因為這兩個方法相互之間是異步運行的。由Windows調度器決定先調度哪一個線程。 三、執行上下文 每個線程都關聯了一個執行上下文數據結構。執行上下文(execution context)包括的東西有安全設置(壓縮棧、Thread的Principal屬性[指示線程的調度優先級]和Windows身份)、宿主設置(參見System.Threading.HostExecutionContextManager[提供使公共語言運行時宿主可以參與執行上下文的流動(或移植)的功能])和邏輯調用上下文數據(參見System.Runtime.Remoting.Messaging.CallContext[提供與執行代碼路徑一起傳送的屬性集]的LogicalSetData[將一個給定對象存儲在邏輯調用上下文中并將該對象與指定名稱相關聯]和LogicalGetData[從邏輯調用上下文中檢索具有指定名稱的對象]). 線程執行代碼時,有的操作會受到線程的執行上下文設置(尤其是安全設置)的影響。理想情況下,每當一個線程(初始線程)使用另一個線程(輔助線程)執行任務時,前者的執行上下文應該"流動"(復制)到輔助線程。這就確保輔助線程執行的任何操作使用的都是相同的安全設置和宿主設置。還確保了初始線程的邏輯調用上下文可以在輔助線程中使用。 默認情況下,CLR自動造成初始線程的執行上下文會"流動"(復制)到任何輔助線程。這就是將上下文信息傳輸到輔助線程,但這對損失性能,因為執行上下文中包含大量信息,而收集這些信息,再將這些信息復制到輔助線程,要耗費不少時間。如果輔助線程又采用更多的輔助線程,還必須創建和初始化更多的執行上下文數據結構。 System.Threading命名空間中有一個ExecutionContext類[管理當前線程的執行上下文],它允許你控制線程的執行上下文如何從一個線程"流動"(復制)到另一個線程。下面展示了這個類的樣子:
public sealed class ExecutionContext : IDisposable, ISerializable { [SecurityCritical] //取消執行上下文在異步線程之間的流動 public static AsyncFlowControl SuppressFlow(); //恢復執行上下文在異步線程之間的流動 public static void RestoreFlow(); //指示當前是否取消了執行上下文的流動。 public static bool IsFlowSuppressed(); //不常用方法沒有列出 } 可用這個類阻止一個執行上下文的流動,從而提升應用程序的性能。對于服務器應用程序,性能的提升可能非常顯著。但是,客戶端應用程序的性能提升不了多少。另外,由于SuppressFlow方法用[SecurityCritical]attribute進行了標識,所以在某些客戶端應用程序(比如Silverlight)中是無法調用的。當然,只有在輔助線程不需要或者不防問上下文信息時,才應該組織執行上下文的流動。如果初始線程的執行上下文不流向輔助線程,輔助線程會使用和它關聯起來的任何執行上下文。在這種情況下,輔助線程不應該執行要依賴于執行上下文狀態(比如用戶的Windows身份)的代碼。 注意:添加到邏輯調用上下文的項必須是可序列化的。對于包含了邏輯調用上下文數據線的一個執行上下文,如果讓它流動,可能嚴重損害性能,因為為了捕捉執行上下文,需對所有數據項進行序列化和反序列化。 下例展示了向CLR的線程池隊列添加一個工作項的時候,如何通過阻止執行上下文的流動來影響線程邏輯調用上下文中的數據:class Program { static void Main(string[] args) { // 將一些數據放到Main線程的邏輯調用上下文中 CallContext.LogicalSetData("Name", "Jeffrey"); // 線程池能訪問到邏輯調用上下文數據,加入到程序池隊列中 ThreadPool.QueueUserWorkItem( state => Console.WriteLine("Name={0}", CallContext.LogicalGetData("Name"))); // 現在阻止Main線程的執行上下文流動 ExecutionContext.SuppressFlow(); //再次訪問邏輯調用上下文的數據 ThreadPool.QueueUserWorkItem( state => Console.WriteLine("Name={0}", CallContext.LogicalGetData("Name"))); //恢復Main線程的執行上下文流動 ExecutionContext.RestoreFlow(); //再次訪問邏輯調用上下文的數據 ThreadPool.QueueUserWorkItem( state => Console.WriteLine("Name={0}", CallContext.LogicalGetData("Name"))); Console.Read(); } } 會得到一下結果:Name=JeffreyName=Name=Jeffrey 雖然現在我們討論的是調用ThreadPool.QueueUserWorkItem時阻止執行上下文的流動,但在使用Task對象(以后會提到),以及在發起異步I/O操作(以后會提到)時也會用到。 四、協作式取消 Microsoft .NET Framework提供了一個標準的取消操作模式。這個模式是協作式的,意味著你想取消的操作必須顯式的支持取消。換言之,無論執行操作的代碼,還是試圖取消操作的代碼,都必須使用本節提到的類型。對于長時間 運行的計算限制操作來說,支持取消是一件非常"棒"的事。所以,你應該考慮為自己的計算限制操作添加取消能力。 首先,先解釋一下FCL提供的兩個主要類型,它們是標準協作式取消模式的一部分。 為了取消一個操作,首先必須創建一個System.Thread.CancellationTokenSource[通知CancellationToken,告知其應被取消]對象。這個類如下所示: public class CancellationTokenSource : IDisposable { //構造函數 public CancellationTokenSource(); //獲取是否已請求取消此 System.Threading.CancellationTokenSource public bool IsCancellationRequested { get; } //獲取與此 System.Threading.CancellationTokenSource 關聯的 System.Threading.CancellationToken public CancellationToken Token; //傳達取消請求。 public void Cancel(); //傳達對取消的請求,并指定是否應處理其余回調和可取消操作。 public void Cancel(bool throwOnFirstException); ... } 這個對象包含了管理取消有關的所有狀態。構造好一個CancellationTokenSource(引用類型)之后,可以從它的Token屬性獲得一個或多個CancellationToken(值類型)實例,并傳給你的操作,使那些操作可以取消。以下是CancellationToken值類型最有用的一些成員: public struct CancellationToken //一個值類型 { //獲取此標記是否能處于已取消狀態,IsCancellationRequested 由非通過Task來調用(invoke)的一個操作調用(call) public bool IsCancellationRequested { get; } //如果已請求取消此標記,則引發 System.OperationCanceledException,由通過Task來調用的操作調用 public void ThrowIfCancellationRequested(); //獲取在取消標記時處于有信號狀態的 System.Threading.WaitHandle,取消時,WaitHandle會收到信號 public WaitHandle WaitHandle { get; } //返回空 CancellationToken 值。 public static CancellationToken None //注冊一個將在取消此 System.Threading.CancellationToken 時調用的委托。省略了簡單重載版本 public CancellationTokenRegistration Register(Action<object> callback, object state, bool useSynchronizationContext); //省略了GetHashCode、Equals成員 } CancellationToken實例是一個輕量級的值類型,它包含單個私有字段:對它的CancellationTokenSource對象的一個引用。在一個計算限制操作的循環中,可以定時調用CancellationToken的IsCancellationRequested屬性,了解循環是否應該提前終止,進而終止計算限制的操作。當然,提前終止的好處在于,CPU不再需要把時間浪費在你對其結果已經不感興趣的一個操作上。現在,用一些示例代碼演示一下: class Program { static void Main(string[] args) { CancellationTokenSource cts = new CancellationTokenSource(); // 將CancellationToken和"要循環到的目標數"傳入操作中 ThreadPool.QueueUserWorkItem(o => Count(cts.Token, 1000)); Console.WriteLine("Press <Enter> to cancel the operation."); Console.ReadLine(); cts.Cancel(); // 如果Count方法已返回,Cancel沒有任何效果 // Cancel立即返回,方法從這里繼續運行 Console.ReadLine(); } private static void Count(CancellationToken token, Int32 countTo) { for (Int32 count = 0; count < countTo; count++) { //判斷是否接收到了取消任務的信號 if (token.IsCancellationRequested) { Console.WriteLine("Count is cancelled"); break; // 退出循環以停止操作 } Console.WriteLine(count);
新聞熱點
疑難解答