在開發偶爾會遇到需要起多個 Task ,接著等待這些 Task 都完成在去做後續邏輯處理,.NET 中提供 Task.WaitAll 與 Task.WhenAll 靜態方法來知道所有任務是否執行完成,過去自己對於兩者的差異性不太明白,因此這篇文章整理自己對於兩者的相關資訊與用法,希望有不清楚或是自己研究錯誤的地方歡迎提出討論
探索問題
Task.WaitAll
在以下的 Sample Code 中使用 Task.Run 建立三個 Task 分別 sleep 1、2、3 秒鐘,接著使用 Task.WaitAll 方法來知道三者是否已執行完成
static void Main(string[] args) { Task Task1 = Task.Run(() => Thread.Sleep(1000)); Task Task2 = Task.Run(() => Thread.Sleep(2000)); Task Task3 = Task.Run(() => Thread.Sleep(3000)); Task.WaitAll(Task1, Task2, Task3); // todo something.. }
在 Task.WaitAll 有提供另一組 API ,可以限定想要等待的時間秒數才不用一直無止境等待下去,這裡在原本的 sample code 再加上等待時間 2.5 秒及透過 Task.IsCompleted 顯示各自是否已完成
在非同步情境時使用 WaitAll 會阻礙執行緒或鎖定 ( blocks thread ),會造成在所有的工作結束之前,當前使用到的執行緒無法自由處理其他工作,在此篇 How and Where Concurrent Asynchronous I/O with ASP.NET Web API 文章有提到,如果某項任務無法正確執行最後引起 deadlocks 狀況發生,此時需要使用 ConfigureAwait 來避免執行緒 lock,更詳細的內容可以參考 Best Practices in Asynchronous Programming。
- static void Main(string[] args)
- {
- Task Task1 = Task.Run(() => Thread.Sleep(1000));
- Task Task2 = Task.Run(() => Thread.Sleep(2000));
- Task Task3 = Task.Run(() => Thread.Sleep(3000));
- Task.WaitAll(new Task[] {Task1, Task2, Task3 }, 2500);
- Console.WriteLine("Task1.IsCompleted:{0}", Task1.IsCompleted);
- Console.WriteLine("Task2.IsCompleted:{0}", Task2.IsCompleted);
- Console.WriteLine("Task3.IsCompleted:{0}", Task3.IsCompleted);
- }
- // result
- // Task1.IsCompleted:True
- // Task2.IsCompleted:True
- // Task3.IsCompleted:False
Task.WhenAll
另一個等待所有任務完成的方法是 Task.WhenAll,使用的 Sample Code 中是相同於上述代碼,使用方式不難,在 MSDN Task.Whenall 方法簽章可以看到,使用 Task.WhenAll 方法時會回傳 Task,因此與剛剛差異的是其中等待完成任務方法使用 WhenAll 進行,在使用一個 taskWhenAll 變數用 wait 方法等待完成
在執行到 Task.WhenAll 時候,會新增一個 Task 並等待該任務的結果 (自己理解上是有專門 Task 在 handle 後續處理),因此使用 WhenAll 不會造成執行緒阻礙的情況發生
static void Main(string[] args) { Task Task1 = Task.Run(() => Thread.Sleep(1000)); Task Task2 = Task.Run(() => Thread.Sleep(2000)); Task Task3 = Task.Run(() => Thread.Sleep(3000)); var taskWhenAll = Task.WhenAll(Task1, Task2, Task3); taskWhenAll.Wait(); }
Task.WaitAll v.s Task.WhenAll
上面分別介紹兩者的用法與說明,但光看文字與簡單代碼還不過癮,因此小弟參考網路上資料針對 WaitAll 與 WhenAll 執行時間做比較讓數據說話,sample Code 如下
public class Program { static void Main(string[] args) { IEnumerable<Worker> workerObjects = new List<Worker> { new Worker {Id = 1, SleepTimeout = 1000}, new Worker {Id = 2, SleepTimeout = 2000}, new Worker {Id = 3, SleepTimeout = 3000}, new Worker {Id = 4, SleepTimeout = 4000}, new Worker {Id = 5, SleepTimeout = 5000}, }; // WaitAll TaskWaitAll(workerObjects); // WhenAll var task = TaskWhenAll(workerObjects); Console.ReadKey(); } static void TaskWaitAll(IEnumerable<Worker> workers) { var startTime = DateTime.Now; Console.WriteLine("Starting : Task.WaitAll..."); Task.WaitAll(workers.Select(worker => worker.DoWork(startTime)).ToArray()); var endTime = DateTime.Now; Console.WriteLine("Test finished after {0:F2} seconds.\n", (endTime - startTime).TotalSeconds); } static Task TaskWhenAll(IEnumerable<Worker> workers) { var startTime = DateTime.Now; Console.WriteLine("Starting test: Task.WhenAll..."); var task = Task.WhenAll(workers.Select(worker => worker.DoWork(startTime))); task.Wait(); var endTime = DateTime.Now; Console.WriteLine("Test finished after {0:F2} seconds.\n", (endTime - startTime).TotalSeconds); return task; } } public class Worker { public int Id; public int SleepTimeout; public async Task DoWork(DateTime testStart) { var workerStart = DateTime.Now; Console.WriteLine("Worker {0} started on thread {1}, beginning {2:F2} seconds after test start.", Id, Thread.CurrentThread.ManagedThreadId, (workerStart - testStart).TotalSeconds); await Task.Run(() => Thread.Sleep(SleepTimeout)); var workerEnd = DateTime.Now; Console.WriteLine("Worker {0} stopped; the worker took {1:F2} seconds, and it finished {2:F2} seconds after the test start.", Id, (workerEnd - workerStart).TotalSeconds, (workerEnd - testStart).TotalSeconds); } }
簡單說明 sample code 內容
- Worker 類別 : 提供 doWork async方法透過 task.run 執行 thread.sleep,可傳入要 sleep 時間與 id,執行前後分別記錄起始、結束時間
- main : 主要測試程式進入點,做三件事情
- 初始化測試的 workerObjects,建立五筆 worker instance 分別測試 1~5 秒
- 執行測試 TaskWaitAll、TaskWhenAll 方法
- TaskWaitAll 方法,裡面紀錄 waitAll 方法起始與結束的時間
- TaskWhenAll 方法,裡面紀錄 whenAll 方法起始與結束的時間
執行結果
從以下得知 Task.WaitAll 執行時間為 5.07 秒,Task.WhenAll 執行時間為 5.01 秒執行多次時間比較都是 WhenAll 會優於 WaitAll,有興趣的客官可以自行下載試試
為了避免執行緒阻塞的情形發生,使用上建議 Task.WhenAll 來取代 Task.WaitAll,從最後的簡單測試代碼執行時間比較來看也是 WhenAll 會優於 WaitAll,其中為了比較 Task.waitAll 與 Task.whenAll 差異性閱讀很多相關的文章與 blog,花了很多時間才產生這篇文章,希望可以透過以上的說明能幫助到有需要的網友
Async/Await - Best Practices in Asynchronous Programming
Using async/await for multiple tasks
How and Where Concurrent Asynchronous I/O with ASP.NET Web API
await, WhenAll, WaitAll, oh my!!
https://www.youtube.com/watch?v=dGyr6hiy-lE
回覆刪除Task.WhenEach