推薦幾篇寫的很好的文章,本文部分轉自
https://blog.csdn.net/btfireknight/article/details/97766193
https://blog.csdn.net/boonya/article/details/80541571
https://blog.csdn.net/nacl025/article/details/9163495/
1. Task 原理
這里簡要的分析下CLR線程池,其實線程池中有一個叫做“全局隊列”的概念,每一次我們使用QueueUserWorkItem的使用都會產生一個“工作項”,然后“工作項”進入“全局隊列”進行排隊,最后線程池中的的工作線程以FIFO(First Input First Output)的形式取出,這里值得一提的是在.net 4.0之后“全局隊列”采用了無鎖算法,相比以前版本鎖定“全局隊列”帶來的性能瓶頸有了很大的改觀。那么任務委托的線程池不光有“全局隊列”,而且每一個工作線程都有”局部隊列“。我們的第一反應肯定就是“局部隊列“有什么好處呢?這里暫且不說,我們先來看一下線程池中的任務分配,如下圖:
線程池的工作方式大致如下,線程池的最小線程數是6,線程1~3正在執行任務1~3,當有新的任務時,就會向線程池請求新的線程,線程池會將空閑線程分配出去,當線程不足時,線程池就會創建新的線程來執行任務,直到線程池達到最大線程數(線程池滿)。總的來說,只有有任務就會分配一個線程去執行,當FIFO十分頻繁時,會造成很大的線程管理開銷。
下面我們來看一下task中是怎么做的,當我們new一個task的時候“工作項”就會進去”全局隊列”,如果我們的task執行的非常快,那么“全局隊列“就會FIFO的非常頻繁,那么有什么辦法緩解呢?當我們的task在嵌套的場景下,“局部隊列”就要產生效果了,比如我們一個task里面有3個task,那么這3個task就會存在於“局部隊列”中,如下圖的任務一,里面有三個任務要執行,也就是產生了所謂的"局部隊列",當任務三的線程執行完成時,就會從任務一種的隊列中以FIFO的形式"竊取"任務執行,從而減少了線程管理的開銷。這就相當於,有兩個人,一個人干完了分配給自己的所有活,而另一個人卻還有很多的活,閑的人應該接手點忙的人的活,一起快速完成。
從上面種種情況我們看到,這些分流和負載都是普通ThreadPool.QueueUserWorkItem所不能辦到的,所以說在.net 4.0之后,我們盡可能的使用TPL,拋棄ThreadPool
2. Task細節
Task的屬性IsCompleted, IsCanceled表示它是否完成和是否取消
具體的property參考官方API: https://docs.microsoft.com/en-us/dotnet/api/system.threading.tasks.task?view=netcore-3.1
Async: 當一個方法由async關鍵字標識,表明這個方法是異步方法,當它被調用時,會創建一個線程來執行
Async 只能修飾void,Task,Task<>
(1) Task創建
static void Main(string[] args) { //1.new方式實例化一個Task,需要通過Start方法啟動 Task task = new Task(() => { Thread.Sleep(100); Console.WriteLine($"hello, task1的線程ID為{Thread.CurrentThread.ManagedThreadId}"); }); task.Start(); //2.Task.Factory.StartNew(Action action)創建和啟動一個Task Task task2 = Task.Factory.StartNew(() => { Thread.Sleep(100); Console.WriteLine($"hello, task2的線程ID為{ Thread.CurrentThread.ManagedThreadId}"); }); //3.Task.Run(Action action)將任務放在線程池隊列,返回並啟動一個Task Task task3 = Task.Run(() => { Thread.Sleep(100); Console.WriteLine($"hello, task3的線程ID為{ Thread.CurrentThread.ManagedThreadId}"); }); Console.WriteLine("執行主線程!"); Console.ReadKey(); }
(2) Task的取消以及取消回調方法
Task中有一個專門的類 CancellationTokenSource 來取消任務執行,CancellationTokenSource的功能不僅僅是取消任務執行,我們可以使用 source.CancelAfter(5000)
實現5秒后自動取消任務,也可以通過 source.Token.Register(Action action)
注冊取消任務觸發的回調函數,即任務被取消時注冊的action會被執行。
static void Main(string[] args) { CancellationTokenSource source = new CancellationTokenSource(); //注冊任務取消的事件 source.Token.Register(() => { Console.WriteLine("任務被取消后執行xx操作!"); }); int index = 0; //開啟一個task執行任務 Task task1 = new Task(() => { while (!source.IsCancellationRequested) { Thread.Sleep(1000); Console.WriteLine($"第{++index}次執行,線程運行中..."); } }); task1.Start(); //延時取消,效果等同於Thread.Sleep(5000);source.Cancel(); source.CancelAfter(5000); Console.ReadKey(); }
查看結果
(3) 實例分析
static void Main(string[] args) { Console.WriteLine("111 balabala. My Thread ID is :" + Thread.CurrentThread.ManagedThreadId); AsyncMethod(); SyncMethod(); Thread.Sleep(10000); Console.WriteLine("222 balabala. My Thread ID is :" + Thread.CurrentThread.ManagedThreadId); } private static async Task AsyncMethod() { Console.WriteLine("Helo I am AsyncMethod. My Thread ID is :" + Thread.CurrentThread.ManagedThreadId); var ResultFromTimeConsumingMethod = TimeConsumingMethod(); string Result = await ResultFromTimeConsumingMethod + " + AsyncMethod. My Thread ID is :" + Thread.CurrentThread.ManagedThreadId; Console.WriteLine(Result); //返回值是Task的函數可以不用return } private static Task SyncMethod() { var task = Task.Run(() => { Console.WriteLine("Helo I am SyncMethod. My Thread ID is :" + Thread.CurrentThread.ManagedThreadId); Thread.Sleep(5000); Console.WriteLine("Helo I am SyncMethod after Sleep(5000). My Thread ID is :" + Thread.CurrentThread.ManagedThreadId); }); return task; } //這個函數就是一個耗時函數,可能是IO操作,也可能是cpu密集型工作。 private static Task<string> TimeConsumingMethod() { var task = Task.Run(() => { Console.WriteLine("Helo I am TimeConsumingMethod. My Thread ID is :" + Thread.CurrentThread.ManagedThreadId); Thread.Sleep(5000); Console.WriteLine("Helo I am TimeConsumingMethod after Sleep(5000). My Thread ID is :" + Thread.CurrentThread.ManagedThreadId); return "Hello I am TimeConsumingMethod"; }); return task; }
執行結果如下:
Main 函數里面增加Thread.Sleep(10000)是防止主線程結束,一旦主線程結束了,那么其他線程也釋放了。
如圖可以看出這幾個關鍵字的真正含義
1. 當執行返回參數為Task或者Task<>類型的函數時,假如該函數沒有用async標識,那么開啟線程執行開方法
2. 當有async標識時,當前線程會把該方法當成同步函數執行,直到運行到await關鍵字的地方,開啟新線程(此時假如中途執行另一個Task標識的方法,不管該方法是不是async,都會同步執行,不會開啟新線程, 但是加入把一個task得方法放到變量中,會開啟新的線程,這里非常重要。看如下代碼)
public async Task Test() { await xxx; // 這里會在當前task得線程中執行RunOtherTask方法,並不會開啟新的task RunOtherTask(); // 這里主線程會繼續執行下面得代碼,開啟一個新的線程執行RunOtherTask _ = RunOtherTask(); await otherLogic } private Task RunOtherTask() { return Task.Run(() => { for (var i = 0; i < 100000; i++) { XXX } }); }
3. await關鍵字表示會開辟新線程來執行后面的方法,但是該線程會等待新線程執行完返回,然后繼續執行
函數的執行途中是根據await關鍵字來判斷是否需要開辟線程來執行代碼(Async void方法調用時不能加await,所以它必定是在主線程中被調用),假如被調用的method前面有await,那么這個method必須包含async關鍵字,假如一個async標識的方法里面沒有await,那么這個方法會被當成同步方法來調用
3. Task關鍵點
Async void 主要用於異步事件處理方法,其他時候請不要使用,在async void方法中,一定要加try catch來捕捉異常。
Async void 方法具有不同的錯誤處理語義。 當 async Task 或 async Task<T> 方法引發異常時,會捕獲該異常並將其置於 Task 對象上。 對於 async void 方法,沒有 Task 對象,因此 async void 方法引發的任何異常都會直接在 SynchronizationContext(在 async void 方法啟動時處於活動狀態)上引發。 無法捕獲從 async void 方法引發的異常。所以對於Async void方法必須加入try/catch。
Async void 方法具有不同的組合語義。 返回 Task 或 Task<T> 的 async 方法可以使用 await、Task.WhenAny、Task.WhenAll 等方便地組合而成。 返回 void 的 async 方法未提供一種簡單方式,用於向調用代碼通知它們已完成。 啟動幾個 async void 方法不難,但是確定它們何時結束卻不易。 Async void 方法會在啟動和結束時通知 SynchronizationContext,但是對於常規應用程序代碼而言,自定義 SynchronizationContext 是一種復雜的解決方案。
Async void 方法難以測試。 由於錯誤處理和組合方面的差異,因此調用 async void 方法的單元測試不易編寫。 MSTest 異步測試支持僅適用於返回 Task 或 Task<T> 的 async 方法。 可以安裝 SynchronizationContext 來檢測所有 async void 方法都已完成的時間並收集所有異常,不過只需使 async void 方法改為返回 Task,這會簡單得多。推薦使用下面方法實現
private async void button1_Click(object sender, EventArgs e)
{
await Button1ClickAsync(); } public async Task Button1ClickAsync() { // Do asynchronous work. await Task.Delay(1000); }
應避免混合使用異步代碼和阻塞代碼。 混合異步代碼和阻塞代碼可能會導致死鎖、更復雜的錯誤處理及上下文線程的意外阻塞,推薦除了main方法外都使用async方法,不要再異步代碼使用Task.Result和Task.Wait。並且推薦使用ConfigureAwait(false)。
還沒有完全理解內部的原理,請看下面的鏈接
https://blog.csdn.net/WPwalter/article/details/79673214
http://blog.walterlv.com/post/deadlock-in-task-wait.html
4. async和Lambda
async Action == async void
async Func<string> == async Task<string>
當一個Action或者Func的類型是async void,並且作為參數傳遞到另一個方法中,當執行另一個方法時,並不能等待Action執行完再繼續
看代碼
public Task ExecuteAction(Action action) { Console.WriteLine("In ExecuteAction = " + Thread.CurrentThread.ManagedThreadId); action(); Console.WriteLine("In ExecuteAction = " + Thread.CurrentThread.ManagedThreadId); TestAsync(); Console.WriteLine("In ExecuteAction = " + Thread.CurrentThread.ManagedThreadId); return Task.CompletedTask; } public async Task ExecuteAwaitAction(Action action) { Console.WriteLine("In ExecuteAwaitAction = " + Thread.CurrentThread.ManagedThreadId); await Task.Run(action); await TestAsync(); } private static void Main(string[] args) { try { Console.WriteLine("In main = " + Thread.CurrentThread.ManagedThreadId); Test(); } catch (Exception e) { Console.WriteLine(e); throw; } Console.ReadKey(); } private async Task TestAsync() { Console.WriteLine("In Delay = " + Thread.CurrentThread.ManagedThreadId); await Task.Delay(3000); Console.WriteLine("In Delay = " + Thread.CurrentThread.ManagedThreadId); } public static async void Test() { Console.WriteLine("In Test = " + Thread.CurrentThread.ManagedThreadId); IActionTest actionTest = new ActionTest(); await actionTest.ExecuteAction( TestAwait); //await actionTest.ExecuteAwaitAction( () => //{ // TestAwait(); //}); var a = 1; } public static async void TestAwait() { Console.WriteLine("In TestAwait = " + Thread.CurrentThread.ManagedThreadId); await Testsss(); int a = 3; a++; } public static async Task Testsss() { Console.WriteLine("In Testsss = " + Thread.CurrentThread.ManagedThreadId); await Task.Run(() => { Console.WriteLine("In lambda = " + Thread.CurrentThread.ManagedThreadId); int ctr = 0; for (ctr = 0; ctr <= 1000000000; ctr++) { } Console.WriteLine("Finished {0} loop iterations", ctr); }); //下面的方法會將其當成void方法 //int ctr = 0; //for (ctr = 0; ctr <= 1000000000; ctr++) //{ } //Console.WriteLine("Finished {0} loop iterations", // ctr); }
最后的結果是不管是在ExecutAction還是ExecutAwaitAction里面,action方法都不會等待,會直接執行下面的test方法,因為action本身就是異步方法,而在實現ExecutAction不能實現await Action,所以會立即返回。寫代碼時要注意當需要使用Func的返回值時,這種形式是有問題的。
5. 判斷Task超時的方法
用Task.Delay(ElapsedMilliseconds, _cancellationTokenSource.Token);而不用Task.Delay(ElapsedMilliseconds); 因為后者會卡住task固定的時常,但是用前者可以隨時取消。
/// <summary> /// Gets another task which that the given task <paramref name="self"/> can be awaited with a <paramref name="timeout"/>. /// </summary> /// <param name="self">The task to be awaited.</param> /// <param name="timeout">The number of milliseconds to wait.</param> /// <returns> /// <c>true</c> if the <see cref="Task"/> completed execution within the allotted time; otherwise, <c>false</c>. /// </returns> public static async Task<bool> GetTaskWithTimeout(this Task self, int timeout) { var timeoutTask = Task.Delay(timeout); var finishedTask = await Task.WhenAny(self, timeoutTask); // If the returned task is the return ReferenceEquals(finishedTask, self); }
6. 使用CancellationTokenSource創建一個定時輪詢的service, 本機測試的是每小時查詢一次電壓,假如過低就記錄日志,並且只記錄一次
private const int ElapsedMilliseconds = 3600000;private const int StopTaskTimeout = 2000; private bool _isBatteryLowShown; private readonly CancellationTokenSource _cancellationTokenSource = new CancellationTokenSource();private Task _checkingStatusTask; /// <summary> /// Starts the service. /// </summary> public Task StartAsync() { _isBatteryLowShown = false; _checkingStatusTask = RunCheckBatteryStatusPeriodicTask(); return Task.CompletedTask; } /// <summary> /// Stops the service. /// </summary> public async Task StopAsync() { _cancellationTokenSource.Cancel(); await _checkingStatusTask.GetTaskWithTimeout(StopTaskTimeout); if (!_checkingStatusTask.IsCompleted) { _logger.Warning($"Failed to stop checking status task within {StopTaskTimeout} ms - stopping anyway."); } } private async Task RunCheckBatteryStatusPeriodicTask() { try { while (!_cancellationTokenSource.IsCancellationRequested) { var status = xxx(); if (status && !_isBatteryLowShown) { _isBatteryLowShown = true; _logger.Error("Battery is low."); } await Task.Delay(ElapsedMilliseconds, _cancellationTokenSource.Token); } } catch (Exception e) { _logger.ErrorEx(message: $"{nameof(_checkingStatusTask)} exception.", sourceType: nameof(BatteryMonitoringService), ex: e); } }