.NET Framework 4.5 開始引入 Task.Run,它可以很方便的幫助我們使用 async / await 語法,同時還使用線程池來幫助我們管理線程。以至於我們編寫異步代碼可以像編寫同步代碼一樣方便。
不過,如果濫用,也可能導致應用的性能急劇下降。本文將說明在默認線程池配置(ThreadPoolTaskScheduler)的情況下,應該如何使用 Task.Run 來避免性能的急劇降低。
本文內容
如何使用 Task.Run?
示例程序和示例代碼
TaskScheduler
ThreadPool
推薦的使用方法
參考資料
如何使用 Task.Run?
對於 IO 操作,盡量使用原生提供的 Async 方法(不要自己使用 Task.Run 調用一個同步的版本占用線程池資源);
對於沒有 Async 版本的 IO 操作,如果可能耗時很長,則指定 CreateOptions 為 LongRunning。
其他短時間執行的任務才推薦使用 Task.Run。
接下來分析原因:
示例程序和示例代碼
在開始之前,我們先准備一個測試程序。這個程序一開始就使用 Task.Run 跑起來 10 個異步任務,每一個里面都等待 5 秒。
可以發現,雖然我們是同一時間啟動的 10 個異步任務,但任務的實際開始時間並不相同 —— 前面 8 個任務立刻開始了,而后面每隔一秒才會啟動一個新的異步任務。
示例程序的代碼如下:
class Program
{
static async Task Main(string[] args)
{
Console.Title = "walterlv task demo";
var task = Enumerable.Range(0, 10).Select(i => Task.Run(() => LongTimeTask(i))).ToList();
await Task.WhenAll(task);
Console.Read();
}
private static void LongTimeTask(int index)
{
var threadId = Thread.CurrentThread.ManagedThreadId.ToString().PadLeft(2, ' ');
var line = index.ToString().PadLeft(2, ' ');
Console.WriteLine($"[{line}] [{threadId}] [{DateTime.Now:ss.fff}] 異步任務已開始……");
// 這一句才是關鍵,等待。其他代碼只是為了輸出。
Thread.Sleep(5000);
Console.ForegroundColor = ConsoleColor.Green;
Console.WriteLine($"[{line}] [{threadId}] [{DateTime.Now:ss.fff}] 異步任務已結束……");
Console.ForegroundColor = ConsoleColor.White;
}
}
class Program { static async Task Main(string[] args) { Console.Title = "walterlv task demo"; var task = Enumerable.Range(0, 10).Select(i => Task.Run(() => LongTimeTask(i))).ToList(); await Task.WhenAll(task); Console.Read(); } private static void LongTimeTask(int index) { var threadId = Thread.CurrentThread.ManagedThreadId.ToString().PadLeft(2, ' '); var line = index.ToString().PadLeft(2, ' '); Console.WriteLine($"[{line}] [{threadId}] [{DateTime.Now:ss.fff}] 異步任務已開始……"); // 這一句才是關鍵,等待。其他代碼只是為了輸出。 Thread.Sleep(5000); Console.ForegroundColor = ConsoleColor.Green; Console.WriteLine($"[{line}] [{threadId}] [{DateTime.Now:ss.fff}] 異步任務已結束……"); Console.ForegroundColor = ConsoleColor.White; } } ———————————————— 版權聲明:本文為CSDN博主「walter lv」的原創文章,遵循CC 4.0 BY-SA版權協議,轉載請附上原文出處鏈接及本聲明。 原文鏈接:https://blog.csdn.net/WPwalter/java/article/details/85222818
TaskScheduler
造成以上異步任務不馬上開始的原因,與 Task 使用的 TaskScheduler 有關。默認情況下,Task.Run 使用的是 .NET 提供的默認 Scheduler,可以通過 TaskScheduler.Default 獲取到。
Task 使用 TaskScheduler 來決定何時執行一個異步任務,如果你不設置,默認的實現是 ThreadPoolTaskScheduler。
你可以前往 .NET Core 的源碼頁面查看源碼:ThreadPoolTaskScheduler.QueueTask。
於是,你在線程池中的設置將決定一個 Task 將在何時開啟一個線程執行。
ThreadPool
通過 ThreadPool.GetMinThreads 可以獲得最小的線程數和異步 IO 完成線程數;通過 ThreadPool.GetMaxThreads 來獲得其最大值。通過對應的 set 方法來設置最小值和最大值。
在 ThreadPool.GetMinThreads(Int32, Int32) Method (System.Threading) - Microsoft Docs
線程池按需提供新的工作線程或 I/O 完成線程直到它達到每個類別的最小值。
默認情況下,最小線程數設置為在系統上的處理器數。
當達到最小值時,線程池可以創建該類別中的其他線程或等待,直到一些任務完成。
需求較低時,線程池線程的實際數量可以低於最小值。
於是便會出現我們在本文一開始運行時出現的結果圖。在我的計算機上(八核),最小線程數是 8,於是開始的 8 個任務可以立即開始執行。當達到數量 8 而依然沒有線程完成執行的時候,線程池會嘗試等待任務完成。但是,1 秒后依然沒有任務完成,於是線程池創建了一個新的線程來執行新的任務;接下來是每隔一秒會開啟一個新的線程來執行現有任務。當有任務完成之后,就可以直接使用之前完成了任務的線程繼續完成新的任務。
不過,每個類別創建線程的總數量受到最大線程數限制。
推薦的使用方法
了解到 ThreadPoolTaskScheduler 的默認行為之后,我們可以做這些事情來充分利用線程池帶來的優勢:
對於 IO 操作,盡量使用原生提供的 Async 方法,這些方法使用的是 IO 完成端口,占用線程池中的 IO 線程而不是普通線程(不要自己使用 Task.Run 占用線程池資源);
對於沒有 Async 版本的 IO 操作,如果可能耗時很長,則指定 CreateOptions 為 LongRunning(這樣便會直接開一個新線程,而不是使用線程池)。
其他短時間執行的任務才推薦使用 Task.Run。
————————————————
版權聲明:本文為CSDN博主「walter lv」的原創文章,遵循CC 4.0 BY-SA版權協議,轉載請附上原文出處鏈接及本聲明。
原文鏈接:https://blog.csdn.net/WPwalter/java/article/details/85222818
