C#多執行緒(12):執行緒池
阿新 • • 發佈:2020-04-27
[TOC]
## 執行緒池
執行緒池全稱為託管執行緒池,執行緒池受 .NET 通用語言執行時(CLR)管理,執行緒的生命週期由 CLR 處理,因此我們可以專注於實現任務,而不需要理會執行緒管理。
執行緒池的應用場景:任務並行庫 (TPL)操作、非同步 I/O 完成、計時器回撥、註冊的等待操作、使用委託的非同步方法呼叫和套接字連線。
ThreadPool 有一個 `QueueUserWorkItem()` 方法,該方法接受一個代表使用者非同步操作的委託(名為 WaitCallback ),呼叫此方法傳入委託後,就會進入執行緒池內部佇列中。
WaitCallback 委託的定義如下:
```csharp
public delegate void WaitCallback(object state);
```
現在我們來寫一個簡單的執行緒池示例,再扯淡一下。
```csharp
class Program
{
static void Main(string[] args)
{
ThreadPool.QueueUserWorkItem(MyAction);
ThreadPool.QueueUserWorkItem(state =>
{
Console.WriteLine("任務已被執行2");
});
Console.ReadKey();
}
// state 表示要傳遞的引數資訊,這裡為 null
private static void MyAction(Object state)
{
Console.WriteLine("任務已被執行1");
}
}
```
十分簡單對不對~
這裡有幾個要點:
* 不要將長時間執行的操作放進執行緒池中;
* 不應該阻塞執行緒池中的執行緒;
* 執行緒池中的執行緒都是後臺執行緒(又稱工作者執行緒);
另外,這裡一定要記住 WaitCallback 這個委託。
我們觀察建立執行緒需要的時間:
```csharp
static void Main()
{
Stopwatch watch = new Stopwatch();
watch.Start();
for (int i = 0; i < 10; i++)
new Thread(() => { }).Start();
watch.Stop();
Console.WriteLine("建立 10 個執行緒需要花費時間(毫秒):" + watch.ElapsedMilliseconds);
Console.ReadKey();
}
```
筆者電腦測試結果大約 160。
### 執行緒池執行緒數
執行緒池中的 `SetMinThreads()`和 `SetMaxThreads()` 可以設定執行緒池工作的最小和最大執行緒數。其定義分別如下:
```csharp
// 設定執行緒池最小工作執行緒數執行緒
public static bool SetMinThreads (int workerThreads, int completionPortThreads);
```
```csharp
// 獲取
public static void GetMinThreads (out int workerThreads, out int completionPortThreads);
```
workerThreads:要由執行緒池根據需要建立的新的最小工作程式執行緒數。
completionPortThreads:要由執行緒池根據需要建立的新的最小空閒非同步 I/O 執行緒數。
`SetMinThreads()` 的返回值代表是否設定成功。
```csharp
// 設定執行緒池最大工作執行緒數
public static bool SetMaxThreads (int workerThreads, int completionPortThreads);
```
```csharp
// 獲取
public static void GetMaxThreads (out int workerThreads, out int completionPortThreads);
```
workerThreads:執行緒池中輔助執行緒的最大數目。
completionPortThreads:執行緒池中非同步 I/O 執行緒的最大數目。
`SetMaxThreads()` 的返回值代表是否設定成功。
這裡就不給出示例了,不過我們也看到了上面出現 **非同步 I/O 執行緒** 這個關鍵詞,下面會學習到相關知識。
### 執行緒池執行緒數說明 關於最大最小執行緒數,這裡有一些知識需要說明。在此前,我們來寫一個示例: ```csharp class Program { static void Main(string[] args) { // 不斷加入任務 for (int i = 0; i < 8; i++) ThreadPool.QueueUserWorkItem(state => { Thread.Sleep(100); Console.WriteLine(""); }); for (int i = 0; i < 8; i++) ThreadPool.QueueUserWorkItem(state => { Thread.Sleep(TimeSpan.FromSeconds(1)); Console.WriteLine(""); }); Console.WriteLine(" 此計算機處理器數量:" + Environment.ProcessorCount); // 工作項、任務代表同一個意思 Console.WriteLine(" 當前執行緒池存線上程數:" + ThreadPool.ThreadCount); Console.WriteLine(" 當前已處理的工作項數:" + ThreadPool.CompletedWorkItemCount); Console.WriteLine(" 當前已加入處理佇列的工作項數:" + ThreadPool.PendingWorkItemCount); int count; int ioCount; ThreadPool.GetMinThreads(out count, out ioCount); Console.WriteLine($" 預設最小輔助執行緒數:{count},預設最小非同步IO執行緒數:{ioCount}"); ThreadPool.GetMaxThreads(out count, out ioCount); Console.WriteLine($" 預設最大輔助執行緒數:{count},預設最大非同步IO執行緒數:{ioCount}"); Console.ReadKey(); } } ``` 執行後,筆者電腦輸出結果(我們的執行結果可能不一樣): ``` 此計算機處理器數量:8 當前執行緒池存線上程數:8 當前已處理的工作項數:2 當前已加入處理佇列的工作項數:8 預設最小輔助執行緒數:8,預設最小非同步IO執行緒數:8 預設最大輔助執行緒數:32767,預設最大非同步IO執行緒數:1000 ``` 我們結合執行結果,來了解一些知識點。 執行緒池最小執行緒數,預設是當前計算機處理器數量。另外我們也看到了。當前執行緒池存線上程數為 8 ,因為執行緒池建立後,無論有沒有任務,都有 8 個執行緒存活。 如果將執行緒池最小數設定得過大(`SetMinThreads()`),會導致任務切換開銷變大,消耗更多得效能資源。 如果設定得最小值小於處理器數量,則也可能會影響效能。
很多人不清楚 Task、Task<TResult> 原理,原因是沒有好好了解執行緒池。### ThreadPool 常用屬性和方法 屬性: | 屬性 | 說明 | | ---------------------- | ---------------------------------- | | CompletedWorkItemCount | 獲取迄今為止已處理的工作項數。 | | PendingWorkItemCount | 獲取當前已加入處理佇列的工作項數。 | | ThreadCount | 獲取當前存在的執行緒池執行緒數。 | 方法: | 方法 | 說明 | | ------------------------------------------------------------ | ------------------------------------------------------------ | | BindHandle(IntPtr) | 將作業系統控制代碼繫結到 ThreadPool。 | | BindHandle(SafeHandle) | 將作業系統控制代碼繫結到 ThreadPool。 | | GetAvailableThreads(Int32, Int32) | 檢索由 GetMaxThreads(Int32, Int32) 方法返回的最大執行緒池執行緒數和當前活動執行緒數之間的差值。 | | GetMaxThreads(Int32, Int32) | 檢索可以同時處於活動狀態的執行緒池請求的數目。 所有大於此數目的請求將保持排隊狀態,直到執行緒池執行緒變為可用。 | | GetMinThreads(Int32, Int32) | 發出新的請求時,在切換到管理執行緒建立和銷燬的演算法之前檢索執行緒池按需建立的執行緒的最小數量。 | | QueueUserWorkItem(WaitCallback) | 將方法排入佇列以便執行。 此方法在有執行緒池執行緒變得可用時執行。 | | QueueUserWorkItem(WaitCallback, Object) | 將方法排入佇列以便執行,並指定包含該方法所用資料的物件。 此方法在有執行緒池執行緒變得可用時執行。 | | QueueUserWorkItem(Action, TState, Boolean) | 將 Action 委託指定的方法排入佇列以便執行,並提供該方法使用的資料。 此方法在有執行緒池執行緒變得可用時執行。 | | RegisterWaitForSingleObject(WaitHandle, WaitOrTimerCallback, Object, Int32, Boolean) | 註冊一個等待 WaitHandle 的委託,並指定一個 32 位有符號整數來表示超時值(以毫秒為單位)。 | | SetMaxThreads(Int32, Int32) | 設定可以同時處於活動狀態的執行緒池的請求數目。 所有大於此數目的請求將保持排隊狀態,直到執行緒池執行緒變為可用。 | | SetMinThreads(Int32, Int32) | 發出新的請求時,在切換到管理執行緒建立和銷燬的演算法之前設定執行緒池按需建立的執行緒的最小數量。 | | UnsafeQueueNativeOverlapped(NativeOverlapped) | 將重疊的 I/O 操作排隊以便執行。 | | UnsafeQueueUserWorkItem(IThreadPoolWorkItem, Boolean) | 將指定的工作項物件排隊到執行緒池。 | | UnsafeQueueUserWorkItem(WaitCallback, Object) | 將指定的委託排隊到執行緒池,但不會將呼叫堆疊傳播到輔助執行緒。 | | UnsafeRegisterWaitForSingleObject(WaitHandle, WaitOrTimerCallback, Object, Int32, Boolean) | 註冊一個等待 WaitHandle 的委託,並使用一個 32 位帶符號整數來表示超時時間(以毫秒為單位)。 此方法不將呼叫堆疊傳播到輔助執行緒。 | ### 執行緒池說明和示例 通過 `System.Threading.ThreadPool` 類,我們可以使用執行緒池。 ThreadPool 類是靜態類,它提供一個執行緒池,該執行緒池可用於執行任務、傳送工作項、處理非同步 I/O、代表其他執行緒等待以及處理計時器。
理論的東西這裡不會說太多,你可以參考官方文件地址:https://docs.microsoft.com/zh-cn/dotnet/api/system.threading.threadpool?view=netcore-3.1
這裡就不給出示例了,不過我們也看到了上面出現 **非同步 I/O 執行緒** 這個關鍵詞,下面會學習到相關知識。
### 執行緒池執行緒數說明 關於最大最小執行緒數,這裡有一些知識需要說明。在此前,我們來寫一個示例: ```csharp class Program { static void Main(string[] args) { // 不斷加入任務 for (int i = 0; i < 8; i++) ThreadPool.QueueUserWorkItem(state => { Thread.Sleep(100); Console.WriteLine(""); }); for (int i = 0; i < 8; i++) ThreadPool.QueueUserWorkItem(state => { Thread.Sleep(TimeSpan.FromSeconds(1)); Console.WriteLine(""); }); Console.WriteLine(" 此計算機處理器數量:" + Environment.ProcessorCount); // 工作項、任務代表同一個意思 Console.WriteLine(" 當前執行緒池存線上程數:" + ThreadPool.ThreadCount); Console.WriteLine(" 當前已處理的工作項數:" + ThreadPool.CompletedWorkItemCount); Console.WriteLine(" 當前已加入處理佇列的工作項數:" + ThreadPool.PendingWorkItemCount); int count; int ioCount; ThreadPool.GetMinThreads(out count, out ioCount); Console.WriteLine($" 預設最小輔助執行緒數:{count},預設最小非同步IO執行緒數:{ioCount}"); ThreadPool.GetMaxThreads(out count, out ioCount); Console.WriteLine($" 預設最大輔助執行緒數:{count},預設最大非同步IO執行緒數:{ioCount}"); Console.ReadKey(); } } ``` 執行後,筆者電腦輸出結果(我們的執行結果可能不一樣): ``` 此計算機處理器數量:8 當前執行緒池存線上程數:8 當前已處理的工作項數:2 當前已加入處理佇列的工作項數:8 預設最小輔助執行緒數:8,預設最小非同步IO執行緒數:8 預設最大輔助執行緒數:32767,預設最大非同步IO執行緒數:1000 ``` 我們結合執行結果,來了解一些知識點。 執行緒池最小執行緒數,預設是當前計算機處理器數量。另外我們也看到了。當前執行緒池存線上程數為 8 ,因為執行緒池建立後,無論有沒有任務,都有 8 個執行緒存活。 如果將執行緒池最小數設定得過大(`SetMinThreads()`),會導致任務切換開銷變大,消耗更多得效能資源。 如果設定得最小值小於處理器數量,則也可能會影響效能。
Environment.ProcessorCount 可以確定當前計算機上有多少個處理器數量(例如CPU是四核八執行緒,結果就是八)。
`SetMaxThreads()` 設定的最大工作執行緒數或 I/O 執行緒數,不能小於 `SetMinThreads()` 設定的最小工作執行緒數或 I/O 執行緒數。 設定執行緒數過大,會導致任務切換開銷變大,消耗更多得效能資源。 如果加入的任務大於設定的最大執行緒數,那麼將會進入等待佇列。不能將工作執行緒或 I/O 完成執行緒的最大數目設定為小於計算機上的處理器數。
### 不支援的執行緒池非同步委託 扯淡了這麼久,我們從設定執行緒數中,發現有個 I/O 非同步執行緒數,這個執行緒數限制的是執行非同步委託的執行緒數量,這正是本節要介紹的。 非同步程式設計模型(Asynchronous Programming Model,簡稱 APM),在日常擼碼中,我們可以使用 `async`、`await` 和`Task` 一把梭了事。 .NET Core 不再使用 `BeginInvoke` 這種模式。你可以跟著筆者一起踩坑先。 筆者在看書的時候,寫了這個示例: 很多地方也在使用這種形式的示例,但是在 .NET Core 中用不了,只能在 .NET Fx 使用。。。 ```csharp class Program { private delegate string MyAsyncDelete(out int thisThreadId); static void Main(string[] args) { int threadId; // 不是非同步呼叫 MyMethodAsync(out threadId); // 建立自定義的委託 MyAsyncDelete myAsync = MyMethodAsync; // 初始化非同步的委託 IAsyncResult result = myAsync.BeginInvoke(out threadId, null, null); // 當前執行緒等待非同步完成任務,也可以去掉 result.AsyncWaitHandle.WaitOne(); Console.WriteLine("非同步執行"); // 檢索非同步執行結果 string returnValue = myAsync.EndInvoke(out threadId, result); // 關閉 result.AsyncWaitHandle.Close(); Console.WriteLine("非同步處理結果:" + returnValue); } private static string MyMethodAsync(out int threadId) { // 獲取當前執行緒在託管執行緒池的唯一標識 threadId = Thread.CurrentThread.ManagedThreadId; // 模擬工作請求 Thread.Sleep(TimeSpan.FromSeconds(new Random().Next(1, 5))); // 返回工作完成結果 return "喜歡我的讀者可以關注筆者的部落格歐~"; } } ``` 目前百度到的很多文章也是 .NET FX 時代的程式碼了,要注意 C# 在版本迭代中,對非同步這些 API ,做了很多修改,不要看別人的文章,學完後才發現不能在 .NET Core 中使用(例如我... ...),浪費時間。 上面這個程式碼示例,也從側面說明了,以往 .NET Fx (C# 5.0 以前)中使用非同步是很麻煩的。 .NET Core 是不支援非同步委託的,具體可以看 [https://github.com/dotnet/runtime/issues/16312](https://github.com/dotnet/runtime/issues/16312) 官網文件明明說支援的[https://docs.microsoft.com/zh-cn/dotnet/api/system.iasyncresult?view=netcore-3.1#examples](https://docs.microsoft.com/zh-cn/dotnet/api/system.iasyncresult?view=netcore-3.1#examples),而且示例也是這樣,搞了這麼久,居然不行,我等下一刀過去。 關於為什麼不支援,可以看這裡:[https://devblogs.microsoft.com/dotnet/migrating-delegate-begininvoke-calls-for-net-core/](https://devblogs.microsoft.com/dotnet/migrating-delegate-begininvoke-calls-for-net-core/) 不支援就算了,我們跳過,後面學習非同步時再仔細討論。 ### 任務取消功能 這個取消跟執行緒池池無關。 CancellationToken:傳播有關應取消操作的通知。 CancellationTokenSource:嚮應該被取消的 CancellationToken 傳送訊號。 兩者關係如下: ```csharp CancellationTokenSource cts = new CancellationTokenSource(); CancellationToken token = cts.Token; ``` 這個取消,在於訊號的發生和訊號的捕獲,任務的取消不是實時的。 示例程式碼如下: CancellationTokenSource 例項化一個取消標記,然後傳遞 CancellationToken 進去; 被啟動的執行緒,每個階段都判斷 `.IsCancellationRequested`,然後確定是否停止執行。這取決於執行緒的自覺性。 ```csharp class Program { static void Main() { CancellationTokenSource cts = new CancellationTokenSource(); Console.WriteLine("按下回車鍵,將取消任務"); new Thread(() => { CanceTask(cts.Token); }).Start(); new Thread(() => { CanceTask(cts.Token); }).Start(); Console.ReadKey(); // 取消執行 cts.Cancel(); Console.WriteLine("完成"); Console.ReadKey(); } private static void CanceTask(CancellationToken token) { Console.WriteLine("第一階段"); Thread.Sleep(TimeSpan.FromSeconds(1)); if (token.IsCancellationRequested) return; Console.WriteLine("第二階段"); Thread.Sleep(TimeSpan.FromSeconds(1)); if (token.IsCancellationRequested) return; Console.WriteLine("第三階段"); Thread.Sleep(TimeSpan.FromSeconds(1)); if (token.IsCancellationRequested) return; Console.WriteLine("第四階段"); Thread.Sleep(TimeSpan.FromSeconds(1)); if (token.IsCancellationRequested) return; Console.WriteLine("第五階段"); Thread.Sleep(TimeSpan.FromSeconds(1)); if (token.IsCancellationRequested) return; } } ``` 這個取消標記,在前面的很多同步方式中,都用的上。 ### 計時器 常用的定時器有兩種,分別是:System.Timers.Timer 和 System.Thread.Timer。 `System.Threading.Timer`是一個普通的計時器,它是執行緒池中的執行緒中。 `System.Timers.Timer`包裝了`System.Threading.Timer`,並提供了一些用於在特定執行緒上分派的其他功能。 什麼執行緒安全不安全。。。俺不懂這個。。。不過你可以參考[https://stackoverflow.com/questions/19577296/thread-safety-of-system-timers-timer-vs-system-threading-timer](https://stackoverflow.com/questions/19577296/thread-safety-of-system-timers-timer-vs-system-threading-timer) 如果你想認真區分兩者的關係,可以檢視:[https://web.archive.org/web/20150329101415/https://msdn.microsoft.com/en-us/magazine/cc164015.aspx](https://web.archive.org/web/20150329101415/https://msdn.microsoft.com/en-us/magazine/cc164015.aspx) 兩者主要使用區別: - [System.Timers.Timer](https://docs.microsoft.com/en-us/dotnet/api/system.timers.timer?view=netframework-4.7.2),它會定期觸發一個事件並在一個或多個事件接收器中執行程式碼。 - [System.Threading.Timer](https://docs.microsoft.com/en-us/dotnet/api/system.threading.timer?view=netframework-4.7.2),它定期線上程池執行緒上執行一個回撥方法。 大多數情況下使用 System.Threading.Timer,因為它比較“輕”,另外就是 .NET Core 1.0 時,`System.Timers.Timer` 被取消了,NET Core 2.0 時又回來了。主要是為了 .NET FX 和 .NET Core 遷移方便,才加上去的。所以,你懂我的意思吧。 System.Threading.Timer 其中一個建構函式定義如下: ```csharp public Timer (System.Threading.TimerCallback callback, object state, uint dueTime, uint period); ``` callback:要定時執行的方法; state:要傳遞給執行緒的資訊(引數); dueTime:延遲時間,避免一建立計時器,馬上開始執行方法; period:設定定時執行方法的時間間隔; 計時器示例: ```csharp class Program { static void Main() { Timer timer = new Timer(TimeTask,null,100,1000); Console.ReadKey(); } // public delegate void TimerCallback(object? state); private static void TimeTask(object state) { Console.WriteLine("www.whuanle.cn"); } } ``` Timer 有不少方法,但不常用,可以檢視官方文件:[https://docs.microsoft.com/zh-cn/dotnet/api/system.threading.timer?view=netcore-3.1#methods](https://docs.microsoft.com/zh-cn/dotnet/api/system.threading.timer?view=netcore-3.1#methods)