本章的之前部分內容主要介紹如何在.NET Framework 中使用線程池的概念。現在我們要介紹如何使用C# 實現創建並使用線程池的.NET 應用程序。如之前描述的那樣,System.Threading 命名空間中包含的ThreadPool 類可以被用於在.NET 應用程序中創建一個線程池。
在我們真正編碼之前,我們必須對ThreadPool 類中的兩個重要規則非常清楚。分別是:
1. 每個應用程序域中只能有一個ThreadPool 對象
2. 我們第一次調用ThreadPool.QueueUserWorkItem() 方法時會創建一個ThreadPool 對象,通過一個定時器或者注冊的等待操作調用的回調方法(內部使用應用程序域的線程池)也可以創建一個ThreadPool 對象。
首先,讓我們通過幾個例子來看一下為什么線程池比單獨開啟一個線程要好。在第一個例子(ThreadDemo.cs)中我們將使用獨立線程(相對於線程池來說,以下類同)來啟動兩個長時間運行任務,而在第二個例子(ThreadPoolDemo.cs )中我們將使用一個線程池啟動兩個同樣的任務:
/************************************* /* Copyright (c) 2012 Daniel Dong * * Author:Daniel Dong * Blog: www.cnblogs.com/danielWise * Email: guofoo@163.com * */ using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading; namespace ThreadDemo { class ThreadDemo { public void LongTask1() { for (int i = 0; i <= 999; i++) { Console.WriteLine("Long Task 1 is being executed"); } } public void LongTask2() { for (int i = 0; i <= 999; i++) { Console.WriteLine("Long Task 2 is being executed"); } } static void Main(string[] args) { ThreadDemo td = new ThreadDemo(); for (int i = 0; i < 10; i++) { Thread t1 = new Thread(new ThreadStart(td.LongTask1)); t1.Start(); Thread t2 = new Thread(new ThreadStart(td.LongTask2)); t2.Start(); } Console.Read(); } } }
在上面的例子中,我們使用獨立線程t1 和 t2 啟動了兩個任務LongTask1 和 LongTask2. 需要注意的是我們在一個循環中重復調用線程,目的是為了對操作系統處理能力施壓,通過結果對比,我們可以清楚地看到使用線程池的優勢。下面的ThreadPoolDemo 類顯示了使用ThreadPool 類的代碼:
/************************************* /* Copyright (c) 2012 Daniel Dong * * Author:Daniel Dong * Blog: www.cnblogs.com/danielWise * Email: guofoo@163.com * */ using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading; namespace ThreadPoolDemo { class ThreadPoolDemo { public void LongTask1(object obj) { for (int i = 0; i <= 999; i++) { Console.WriteLine("Long Task 1 is being executed"); } } public void LongTask2(object obj) { for (int i = 0; i <= 999; i++) { Console.WriteLine("Long Task 2 is being executed"); } } static void Main(string[] args) { ThreadPoolDemo tpd = new ThreadPoolDemo(); for (int i = 0; i < 10; i++) { ThreadPool.QueueUserWorkItem(new WaitCallback(tpd.LongTask1)); ThreadPool.QueueUserWorkItem(new WaitCallback(tpd.LongTask2)); } Console.Read(); } } }
讓我們討論一下上面的例子。它包括兩個獨立的任務LongTask1 和 LongTask2, 這兩個任務都是簡單地在一個循環中向控制台顯示消息。將任務操作過程委托給WaitCallback() 方法可以免除為每個獨立任務設置線程屬性的過程並可以使用ThreadPool 啟動這些任務, 具體請看下面的代碼塊:
ThreadPool.QueueUserWorkItem(new WaitCallback(tpd.LongTask1)); ThreadPool.QueueUserWorkItem(new WaitCallback(tpd.LongTask2));
注意QueueUserWorkItem 是ThreadPool 類中的一個靜態方法因而可以由ThreadPool類直接調用。這個例子也有一個Console.Read() 語句,它會讓控制台一直等待用戶輸入回車鍵或其他任意鍵。
通過一個接一個地運行ThreadDemo 和 ThreadPoolDemo 應用程序,我們可以使用任務管理器對兩者進行比較。取決於操作系統的處理能力所以在每個操作系統上結果都可能不同,但是相對結果將會是一樣的。
ThreadDemo 應用程序使用的線程數目圖示:
ThreadPoolDemo 應用程序使用的線程數目圖示:
通過兩個截圖的對比,我可以可以很明顯地發現使用ThreadPool 不僅可以幫助降低應用程序使用的線程數量還可以減小CPU時間和應用程序使用的內存大小。
下一個例子顯示了如何向一個線程池中的線程傳遞參數以及接收它的返回值。線程池架構僅允許我們傳遞一個單一對象參數,但是通常我們向給被一個線程池中的線程執行的方法傳遞多個參數。然而,我們可以很容易地將所有必要的參數包裝到一個類中並將類的實例作為一個參數傳遞給QueueUserWorkItem() 方法:
/************************************* /* Copyright (c) 2012 Daniel Dong * * Author:Daniel Dong * Blog: www.cnblogs.com/danielWise * Email: guofoo@163.com * */ using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading; namespace ThreadPoolState { class ThreadPoolState { public void Task1(object stateObj) { ObjState stObj = (ObjState)stateObj; Console.WriteLine("Input Argument 1 in task 1: " + stObj.inarg1); Console.WriteLine("Input Argument 2 in task 1: " + stObj.inarg2); stObj.outval = "From Task 1 " + stObj.inarg1 + " " + stObj.inarg2; } public void Task2(object stateObj) { ObjState stObj = (ObjState)stateObj; Console.WriteLine("Input Argument 1 in task 2: " + stObj.inarg1); Console.WriteLine("Input Argument 2 in task 2: " + stObj.inarg2); stObj.outval = "From Task 2 " + stObj.inarg1 + " " + stObj.inarg2; } static void Main(string[] args) { ObjState stObj1 = new ObjState(); stObj1.inarg1 = "String Param1 of task 1"; stObj1.inarg2 = "String Param2 of task 1"; ObjState stObj2 = new ObjState(); stObj2.inarg1 = "String Param1 of task 2"; stObj2.inarg2 = "String Param2 of task 2"; ThreadPoolState tps = new ThreadPoolState(); //Queue a task ThreadPool.QueueUserWorkItem(new WaitCallback(tps.Task1), stObj1); //Queue another task ThreadPool.QueueUserWorkItem(new WaitCallback(tps.Task2), stObj2); Console.Read(); } } }
ThreadPoolState 的輸出結果如下:
我們現在來一步一步地分析上面的例子。這個例子與之前的例子非常類似除了傳遞了一個對象;我們使用ObjState 對象向線程池的任務隊列傳遞輸入和輸出參數。
ObjState 對象包含兩個輸入參數和一個輸出參數,所有類型都是String, 如下面代碼塊顯示:
internal class ObjState { protected internal String inarg1; protected internal String inarg2; protected internal String outval; }
下一步我們定義了兩個方法,task1 和 task2, 並為它們分別傳遞了一個ObjState 對象的實例作為參數。task1 和 task2 將輸入參數對象的inarg1 和 inarg2 值組合起來並將結果存儲到outval 變量中。如下面代碼塊所示:
public void Task1(object stateObj) { ObjState stObj = (ObjState)stateObj; Console.WriteLine("Input Argument 1 in task 1: " + stObj.inarg1); Console.WriteLine("Input Argument 2 in task 1: " + stObj.inarg2); stObj.outval = "From Task 1 " + stObj.inarg1 + " " + stObj.inarg2; } public void Task2(object stateObj) { ObjState stObj = (ObjState)stateObj; Console.WriteLine("Input Argument 1 in task 2: " + stObj.inarg1); Console.WriteLine("Input Argument 2 in task 2: " + stObj.inarg2); stObj.outval = "From Task 2 " + stObj.inarg1 + " " + stObj.inarg2; }
在Main() 方法中我們使用ThreadPool.QueueUserWorkItem() 方法在線程池中運行這兩個任務,如下代碼塊所示:
static void Main(string[] args) { ObjState stObj1 = new ObjState(); stObj1.inarg1 = "String Param1 of task 1"; stObj1.inarg2 = "String Param2 of task 1"; ObjState stObj2 = new ObjState(); stObj2.inarg1 = "String Param1 of task 2"; stObj2.inarg2 = "String Param2 of task 2"; ThreadPoolState tps = new ThreadPoolState(); //Queue a task ThreadPool.QueueUserWorkItem(new WaitCallback(tps.Task1), stObj1); //Queue another task ThreadPool.QueueUserWorkItem(new WaitCallback(tps.Task2), stObj2); Console.Read(); }
我們也可以使用ThreadPool.RegisterWaitForSingleObject() 方法來運行有等待操作的任務,這種情況下需要將任務的等待操作傳遞給WaitHandle. WaitHandle 通知包裝到一個WaitOrTimerCallback 委托的方法。在這種情況下,線程池創建一個后台線程調用回調方法。下面的代碼(RegWait.cs)描述了這個概念:
/************************************* /* Copyright (c) 2012 Daniel Dong * * Author:Daniel Dong * Blog: www.cnblogs.com/danielWise * Email: guofoo@163.com * */ using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading; namespace RegWait { public class RegWait { private static int i = 0; static void Main(string[] args) { AutoResetEvent arev = new AutoResetEvent(false); ThreadPool.RegisterWaitForSingleObject( arev, new WaitOrTimerCallback(WorkItem), null, 2000, false); arev.Set(); Console.Read(); } public static void WorkItem(object o, bool signaled) { i += 1; Console.WriteLine("Thread Pool Work Item Invoked: " + i.ToString()); } } }
上面例子的輸入結果與下面類似:
每個兩秒會在控制台打印一行新的結果並輸出遞增變量i 的值,直到用戶輸入回車鍵調用Console.Read() 方法退出為止。
程序一開始會創建一個名為arec 初始值為non-signaled 的AutoResetEvent 對象來通知線程池執行任務組件:
AutoResetEvent arev = new AutoResetEvent(false);
我們調用RegisterWaitForSingleObject() 方法,參數State 值為null, timeout 值為2000毫秒,executeOnceOnly 為false. RegisterWaitForSingleObject() 注冊一個委托並在指定的時間間隔通知工作組件。在我們的例子中,時間間隔設置為兩秒,如下代碼所示:
ThreadPool.RegisterWaitForSingleObject( arev, new WaitOrTimerCallback(WorkItem), null, 2000, false);
為了觸發事件我們需要使用AutoResetEvent對象的Set() 方法:
arev.Set();
這個例子包含了在C# 程序中如何使用線程池的部分內容;下一部分我們將檢查線程池的可擴展性並創建一個線程池管理應用程序。
下一篇介紹一個多線程的微軟消息隊列(MSMQ)監聽器…