C#並行程式設計(2):.NET執行緒池
執行緒Thread
在總結執行緒池之前,先來看一下.NET執行緒。
.NET執行緒與作業系統(Windows)執行緒有什麼區別?
.NET利用Windows的執行緒處理功能。在C#程式編寫中,我們首先會新建一個執行緒物件System.Threading.Thread
,併為其指定一個回撥方法;當我們呼叫執行緒物件的Start
方法啟動執行緒時,會建立一個作業系統執行緒來執行回撥方法。.NET中的執行緒實際上等價於Windows系統執行緒,都是CPU排程和分配的物件。
前臺執行緒和後臺執行緒
.NET把執行緒分為前臺執行緒和後臺執行緒,兩者幾乎相同,唯一的區別是,前臺執行緒會阻止程序的正常退出,後臺執行緒則不會。下面用一個例子描述前、後臺執行緒的區別:
class Program { static void Main(string[] args) { ThreadDemo threadDemo = new ThreadDemo();//間隔500ms持續列印1-20至控制檯 //threadDemo.RunForegroundThread(); //用前臺執行緒列印 threadDemo.RunBackgroundThread(); //用臺執行緒列印 {//把主執行緒掛起5s,然後終止主執行緒 Thread.Sleep(5000); Thread.CurrentThread.Abort(); }//如果用前臺執行緒工作,列印到20才會終止;以後臺執行緒,則不會列印到20 Console.ReadKey(); } } public class ThreadDemo { private readonly Thread _foregroundThread; private readonly Thread _backgroundThread; public ThreadDemo() { this._foregroundThread = new Thread(WriteNumberWorker) { Name = "ForegroundThread"};//預設是前臺執行緒 this._backgroundThread = new Thread(WriteNumberWorker) { Name = "BackgroundThread", IsBackground = true };//後臺執行緒 } /// <summary> /// 模擬執行緒工作 /// </summary> private static void WriteNumberWorker() { for (int i = 0; i < 20; i++) { Console.WriteLine($"{DateTime.Now}=> {Thread.CurrentThread.Name} writes {i + 1}.");//列印從1到20到控制檯 Thread.Sleep(500);//掛起500毫秒,方便測試 } } /// <summary> /// 執行前臺執行緒 /// </summary> public void RunForegroundThread() { this._foregroundThread?.Start(); } /// <summary> /// 執行後臺執行緒 /// </summary> public void RunBackgroundThread() { this._backgroundThread?.Start(); } }
執行緒池ThreadPool
執行緒的建立和銷燬要耗費很多時間,而且過多的執行緒不僅會浪費記憶體空間,還會導致執行緒上下文切換頻繁,影響程式效能。為改善這些問題,.NET執行時(CLR)會為每個程序開闢一個全域性唯一的執行緒池來管理其執行緒。
執行緒池內部維護一個操作請求佇列,程式執行非同步操作時,新增目標操作到執行緒池的請求佇列;執行緒池程式碼提取記錄項並派發給執行緒池中的一個執行緒;如果執行緒池中沒有可用執行緒,就建立一個新執行緒,建立的新執行緒不會隨任務的完成而銷燬,這樣就可以避免執行緒的頻繁建立和銷燬。如果執行緒池中大量執行緒長時間無所事事,空閒執行緒會進行自我終結以釋放資源。
執行緒池通過保持程序中執行緒的少量和高效來優化程式的效能。
C#中執行緒池是一個靜態類,維護兩種執行緒,工作執行緒
和非同步IO執行緒
,這些執行緒都是後臺執行緒。執行緒池不會影響程序的正常退出。
執行緒池的使用
執行緒池提供兩個靜態方法SetMaxThreads
和SetMinThreads
讓我們設定執行緒池的最大執行緒數和最小執行緒數。最大執行緒數指的是,該執行緒池能夠建立的最大執行緒數,當執行緒數達到設定值且忙碌,非同步任務將進入請求佇列,直到有執行緒空閒才會執行;最小執行緒數指的是,執行緒池優先嚐試以設定數量的執行緒處理請求,當請求數達到一定量(未做深入研究)時,才會建立新的執行緒。
下面的例子展示了執行緒池的特性及常見使用方式。
class Program { static void Main(string[] args) { //RunThreadPoolDemo(); RunCancellableWork(); Console.ReadKey(); } static void RunThreadPoolDemo() { ThreadPoolDemo.ThreadPoolDemo.ShowThreadPoolInfo(); ThreadPool.SetMaxThreads(100, 100); // 預設(1023,1000)(8核心CPU) ThreadPool.SetMinThreads(8, 8); // 預設是CPU核心數 ThreadPoolDemo.ThreadPoolDemo.ShowThreadPoolInfo(); ThreadPoolDemo.ThreadPoolDemo.MakeThreadPoolDoSomeWork(100);//計算限制任務 ThreadPoolDemo.ThreadPoolDemo.MakeThreadPoolDoSomeIOWork();//IO限制任務 } static void RunCancellableWork() { Console.WriteLine($"{DateTime.Now}=> Thread-[{Thread.CurrentThread.ManagedThreadId}] started a work"); Console.WriteLine("Press 'Esc' to cancel the work."); Console.WriteLine(); ThreadPoolDemo.ThreadPoolDemo.DoSomeWorkWithCancellation(); if (Console.ReadKey(true).Key == ConsoleKey.Escape) {// 傳送取消通知 ThreadPoolDemo.ThreadPoolDemo.CTSource.Cancel(); } } } public class ThreadPoolDemo { /// <summary> /// 顯示執行緒池資訊 /// </summary> public static void ShowThreadPoolInfo() { int workThreads, completionPortThreads; //當前執行緒池可用的工作執行緒數量和非同步IO執行緒數量 ThreadPool.GetAvailableThreads(out workThreads, out completionPortThreads); Console.WriteLine($"GetAvailableThreads => workThreads:{workThreads};completionPortThreads:{completionPortThreads}"); //執行緒池最大可用的工作執行緒數量和非同步IO執行緒數量 ThreadPool.GetMaxThreads(out workThreads, out completionPortThreads); Console.WriteLine($"GetMaxThreads => workThreads:{workThreads};completionPortThreads:{completionPortThreads}"); //出現新的請求,判斷是否需要建立新執行緒的依據 ThreadPool.GetMinThreads(out workThreads, out completionPortThreads); Console.WriteLine($"GetMinThreads => workThreads:{workThreads};completionPortThreads:{completionPortThreads}"); Console.WriteLine(); } /// <summary> /// 讓執行緒池做些事情 /// </summary> public static void MakeThreadPoolDoSomeWork(int workCount = 10) { for (int i = 0; i < workCount; i++) { int index = i; // 將方法排隊進入執行緒池工作項佇列,當執行緒池有空閒執行緒時執行方法 ThreadPool.QueueUserWorkItem(s => { Thread.Sleep(100); //模擬工作時長 Debug.Print($"{DateTime.Now}=> Thread-[{Thread.CurrentThread.ManagedThreadId}] is running. [{index}]"); ShowAvailableThreads("WorkerThread"); }); } } /// <summary> /// 讓執行緒池做些IO工作 /// </summary> public static void MakeThreadPoolDoSomeIOWork() { //隨便找一些可以訪問的網址 IList<string> uriList = new List<string>() { "http://news.baidu.com/", "https://www.hao123.com/", "https://map.baidu.com/", "https://tieba.baidu.com/", "https://wenku.baidu.com/", "http://fanyi-pro.baidu.com", "http://bit.baidu.com/", "http://xueshu.baidu.com/", "http://www.cnki.net/", "http://www.wanfangdata.com.cn", }; foreach (string uri in uriList) { WebRequest request = WebRequest.Create(uri); request.BeginGetResponse(ac => {// 非同步請求網址,將會利用執行緒池中非同步IO執行緒 try { WebResponse response = request.EndGetResponse(ac); ShowAvailableThreads("IOThread"); Debug.Print($"{DateTime.Now}=> Thread-[{Thread.CurrentThread.ManagedThreadId}] is running. [{response.ContentLength}]"); } catch (Exception ex) { Console.WriteLine(ex.Message); } }, request); } } /// <summary> /// 列印執行緒池當前可用執行緒數 /// </summary> private static void ShowAvailableThreads(string sourceTag = null) { int workThreads, completionPortThreads; ThreadPool.GetAvailableThreads(out workThreads, out completionPortThreads); Console.WriteLine($"{sourceTag} GetAvailableThreads => workThreads:{workThreads};completionPortThreads:{completionPortThreads}"); Console.WriteLine(); } /// <summary> /// 取消通知者 /// </summary> public static CancellationTokenSource CTSource { get; set; } = new CancellationTokenSource(); /// <summary> /// 執行可取消的任務 /// </summary> public static void DoSomeWorkWithCancellation() { ThreadPool.QueueUserWorkItem(t => { Console.WriteLine($"{DateTime.Now}=> Thread-[{Thread.CurrentThread.ManagedThreadId}] begun running. [0 - 9999]"); for (int i = 0; i < 10000; i++) { if (CTSource.Token.IsCancellationRequested) { Console.WriteLine($"{DateTime.Now}=> Thread-[{Thread.CurrentThread.ManagedThreadId}] recived the cancel token. [{i}]"); break; } Thread.Sleep(100);// 模擬工作時長 } Console.WriteLine($"{DateTime.Now}=> Thread-[{Thread.CurrentThread.ManagedThreadId}] was cancelled."); }); } }
執行緒池處理非同步IO的內部原理
上面的例子中,從網站獲取資訊需要用到執行緒池的非同步IO執行緒,執行緒池內部利用IOCP(IO完成埠)與硬體裝置建立連線。非同步IO實現過程如下:
- 託管的IO請求執行緒呼叫Win32原生代碼ReadFile方法
- ReadFile方法分配IO請求包IRP併發送至Windows核心
- Windows核心把收到的IRP放入對應裝置驅動程式的IRP佇列中,此時IO請求執行緒已經可以返回託管程式碼
- 驅動程式處理IRP並將處理結果放入.NET執行緒池的IRP結果佇列中
- 執行緒池分配IO執行緒處理IRP結果
小結
.NET執行緒池是併發程式設計的主要實現方式。C#中Timer
、Parallel
、Task
在內部都是利用執行緒池實現的非同步功能,深入理解執行緒池在並行程式設計中十分重要。