利用線程池可以對線程進行有效的控制,使得線程能夠更好的協作。
在我們實際使用線程時,應當盡量使用線程池來構造線程,避免直接new一個線程。
主要内容:
控制資源消耗
提高線程性能
取消運作中的線程
線程池(ThreadPool)啟動線程的方法很簡單,和上一篇直接new一個線程類似,也有帶參數和不帶參數兩種。
1
2
<code>public</code> <code>static</code> <code>bool</code> <code>QueueUserWorkItem(WaitCallback callBack);</code>
<code>public</code> <code>static</code> <code>bool</code> <code>QueueUserWorkItem(WaitCallback callBack,</code><code>object</code> <code>state);</code>
其中的WaitCallback委托定義如下:
<code>public</code> <code>delegate</code> <code>void</code> <code>WaitCallback(</code><code>object</code> <code>state);</code>
那麼,線程池是如何有效的控制系統資源的消耗的呢?
它的原理非常簡單:
線程池中維護一個請求隊列,當應用程式有異步的請求時,将此請求(比如請求A)發送到線程池。
線程池将請求A放入請求隊列中,然後建立一個線程(比如線程A)來處理請求A。
請求A處理完成後,線程池不會銷毀線程A,而是使用線程A來處理請求隊列中的下一個請求(比如請求B)。
當請求過多時,線程池才會再建立一些線程來加快處理請求隊列中的請求。(注1)
當請求隊列為空時,線程池會銷毀一些空閑時間比較長的線程。(注2)
注1:保證所有的請求由少量線程處理,減少系統資源的消耗,同時減少了線程建立,銷毀的次數。
注2:空閑時間多長才銷毀線程是由CLR決定的,不同版本的CLR這個時間可能不同。
下面通過一個例子來看看,線程池是如何節約系統資源的。
首先看看直接new線程時,系統資源是如何變化的。
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<code>using</code> <code>System;</code>
<code>using</code> <code>System.Threading;</code>
<code>public</code> <code>class</code> <code>CLRviaCSharp_18</code>
<code>{</code>
<code> </code><code>static</code> <code>void</code> <code>Main(</code><code>string</code><code>[] args)</code>
<code> </code><code>{</code>
<code> </code><code>Console.WriteLine(</code><code>"Main Thread"</code><code>);</code>
<code> </code><code>for</code> <code>(</code><code>int</code> <code>i = 0; i < 100; i++)</code>
<code> </code><code>{</code>
<code> </code><code>Thread t =</code><code>new</code> <code>Thread(ThreadMethod);</code>
<code> </code><code>t.Start(i);</code>
<code> </code><code>}</code>
<code> </code><code>Console.ReadKey(</code><code>true</code><code>);</code>
<code> </code><code>}</code>
<code> </code><code>private</code> <code>static</code> <code>void</code> <code>ThreadMethod(</code><code>object</code> <code>state)</code>
<code> </code><code>Console.WriteLine(</code><code>"This thread's state is {0}"</code><code>, state);</code>
<code> </code><code>Thread.Sleep(2000);</code>
<code>}</code>
代碼執行前,系統資源如下圖。線程數和占用的記憶體見下圖的紅色框。
<a href="http://images.cnblogs.com/cnblogs_com/wang_yb/201111/201111071638301681.png"></a>
代碼執行後,系統資源如下圖。線程數和占用的記憶體見下圖的紅色框。
<a href="http://images.cnblogs.com/cnblogs_com/wang_yb/201111/201111071638337828.png"></a>
程式運作時,記憶體一下增加了100多MB,線程也增加了100多個。
在看看利用線程池來處理異步請求時,系統資源是如何變化的。
<code> </code><code>ThreadPool.QueueUserWorkItem(ThreadMethod, i);</code>
<a href="http://images.cnblogs.com/cnblogs_com/wang_yb/201111/201111071638367.png"></a>
<a href="http://images.cnblogs.com/cnblogs_com/wang_yb/201111/201111071638417460.png"></a>
程式運作時,最忙時(剛開始請求隊列中線程較多時)多了10個線程,随着請求隊列中線程的較少,線程池最終隻維持了2,3個線程,消耗的資源最多也就10幾MB。
通過以上的對比,我們可以看出如果應用程式都采用線程池來管理線程的話,确實可以減輕系統的負擔,更有效的利用系統資源,保證多個應用程式可以同時運作。
否則一個應用程式占用太多資源,其他應用程式隻能等待。
當然,通過上面兩個例子,我們也發現利用線程池的程式執行時間比較長。這就是控制資源的結果,使得應用程式的異步請求逐漸處理。
而通過主線程來建立子線程時,主線程的上下文資訊還得拷貝一份再傳入子線程,比如上面的例子中建立了100個線程,就得将上下文資訊拷貝100遍。
在有些情況下,子線程并沒有用到主線程的上下文資訊,此時,我們就可以通過阻止上下文資訊的流動(主線程-->子線程)來提高線程的性能。
下面的例子示範如何阻止和恢複上下文資訊的傳遞。
25
26
27
<code>using</code> <code>System.Runtime.Remoting.Messaging;</code>
<code> </code><code>CallContext.LogicalSetData(</code><code>"Info"</code><code>,</code><code>"Main Thread's Context info."</code><code>);</code>
<code> </code><code>// 阻止上下文的傳遞</code>
<code> </code><code>ExecutionContext.SuppressFlow();</code>
<code> </code><code>ThreadPool.QueueUserWorkItem(ThreadMethod,</code><code>"Thread 1"</code><code>);</code>
<code> </code><code>// 恢複上下文的傳遞</code>
<code> </code><code>ExecutionContext.RestoreFlow();</code>
<code> </code><code>ThreadPool.QueueUserWorkItem(ThreadMethod,</code><code>"Thread 2"</code><code>);</code>
<code> </code><code>Console.WriteLine(state +</code><code>"'s Context Info is : "</code> <code>+ CallContext.LogicalGetData(</code><code>"Info"</code><code>));</code>
運作結果如下:
<a href="http://images.cnblogs.com/cnblogs_com/wang_yb/201111/201111071638448243.png"></a>
對于長時間運作的線程,如果不提供取消操作,那麼它就會一直占用系統資源,直至運作完成。
這樣的使用者體驗很糟糕,是以對于有可能運作很長時間的線程,應該提供取消的操作供使用者選擇。
線程的取消主要使用CancellationTokenSource。
下面通過例子來示範如何取消一個線程的運作。
28
29
30
31
32
<code> </code><code>CancellationTokenSource cts =</code><code>new</code> <code>CancellationTokenSource();</code>
<code> </code><code>ThreadPool.QueueUserWorkItem(o => ThreadMethod(cts.Token));</code>
<code> </code><code>Console.WriteLine(</code><code>"Press any key to cancel."</code><code>);</code>
<code> </code><code>// 取消線程的操作</code>
<code> </code><code>cts.Cancel();</code>
<code> </code><code>private</code> <code>static</code> <code>void</code> <code>ThreadMethod(CancellationToken token)</code>
<code> </code><code>do</code>
<code> </code><code>// 線程取消前一直運作</code>
<code> </code><code>Console.WriteLine(</code><code>"Now is : {0}"</code><code>, DateTime.Now.ToString(</code><code>"HH:mm:ss"</code><code>));</code>
<code> </code><code>Thread.Sleep(1000);</code>
<code> </code><code>}</code><code>while</code> <code>(!token.IsCancellationRequested);</code>
<code> </code><code>Console.WriteLine(</code><code>"This thread is cancelled!"</code><code>);</code>
隻要輸入任意鍵就可以取消線程。
主線程取消子線程後,可能需要進行一些操作來回收資源,釋放對象等等。我們可以将這些操作注冊到CancellationTokenSource的Token中,使得每個線程取消後都會執行這些操作。
33
34
35
36
37
<code> </code><code>// 注冊線程取消後的操作,執行操作的順序與注冊的順利相反</code>
<code> </code><code>// 比如以下2個操作,第二個操作先執行</code>
<code> </code><code>cts.Token.Register(() => Console.WriteLine(</code><code>"sub thread's object is disposed!"</code><code>)); </code><code>// 後執行</code>
<code> </code><code>cts.Token.Register(() => Console.WriteLine(</code><code>"sub thread's garbage is collected!"</code><code>));</code><code>// 先執行</code>
運作結果如下:(在子線程列印了五次時間後,鍵盤輸入任意按鍵)
<a href="http://images.cnblogs.com/cnblogs_com/wang_yb/201111/201111071638466060.png"></a>
3.3 禁止取消線程
為了防止某些線程被意外取消,可以通過CancellationToken.None屬性來禁止某些線程被取消。
<code> </code><code>// 線程"Thread 1"不會被取消</code>
<code> </code><code>ThreadPool.QueueUserWorkItem(o => ThreadMethod(CancellationToken.None,</code><code>"Thread 1"</code><code>));</code>
<code> </code><code>// 線程"Thread 2"會被取消</code>
<code> </code><code>ThreadPool.QueueUserWorkItem(o => ThreadMethod(cts.Token,</code><code>"Thread 2"</code><code>));</code>
<code> </code><code>private</code> <code>static</code> <code>void</code> <code>ThreadMethod(CancellationToken token,</code><code>object</code> <code>state)</code>
<code> </code><code>Console.WriteLine(state +</code><code>" Now is : {0}"</code><code>, DateTime.Now.ToString(</code><code>"HH:mm:ss"</code><code>));</code>
<code> </code><code>Console.WriteLine(state +</code><code>" is cancelled!"</code><code>);</code>
<a href="http://images.cnblogs.com/cnblogs_com/wang_yb/201111/20111107163849256.png"></a>
3.4 關聯多個取消操作
可以将多個取消操作關聯起來,這樣主線程可以很容易的檢驗是否發生了取消操作。
38
39
40
41
42
43
44
45
<code> </code><code>CancellationTokenSource cts1=</code><code>new</code> <code>CancellationTokenSource();</code>
<code> </code><code>CancellationTokenSource cts2=</code><code>new</code> <code>CancellationTokenSource();</code>
<code> </code><code>ThreadPool.QueueUserWorkItem(o => ThreadMethod(cts1.Token,</code><code>"Thread 1"</code><code>));</code>
<code> </code><code>ThreadPool.QueueUserWorkItem(o => ThreadMethod(cts2.Token,</code><code>"Thread 2"</code><code>));</code>
<code> </code><code>// 将ctsLink與cts1和cts2關聯起來</code>
<code> </code><code>CancellationTokenSource ctsLink = CancellationTokenSource.CreateLinkedTokenSource(cts1.Token, cts2.Token);</code>
<code> </code><code>// 取消線程2的操作,cts2.IsCancellationRequested屬性變為True</code>
<code> </code><code>// 同時ctsLink的IsCancellationRequested也變為True</code>
<code> </code><code>cts2.Cancel();</code>
<code> </code><code>// 通過檢驗ctsLink就可以知道是否有線程被取消</code>
<code> </code><code>if</code> <code>(ctsLink.IsCancellationRequested)</code>
<code> </code><code>Console.WriteLine(</code><code>"Some thread has been cancelled!"</code><code>);</code>
<code> </code><code>else</code>
<code> </code><code>Console.WriteLine(</code><code>"No thread has been cancelled!"</code><code>);</code>
本文轉自wang_yb部落格園部落格,原文連結:http://www.cnblogs.com/wang_yb/archive/2011/11/07/2239429.html,如需轉載請自行聯系原作者