對於c#中的async和await的使用,沒想到我一直竟然都有一個錯誤。。
。。還是總結太少,這裡記錄下。
這裡以做早餐為例
流程如下:
- 倒一杯咖啡。
- 加熱平底鍋,然後煎兩個雞蛋。
- 煎三片培根。
- 烤兩片面包。
- 在烤麵包上加黃油和果醬。
- 倒一杯橙汁。
當使用同步方式實現時,程式碼是這樣的:
using System; using System.Diagnostics; using System.Threading.Tasks; namespace AsyncBreakfast { class Program { static void Main(string[] args) { var sw = new Stopwatch(); sw.Start(); Coffee cup = PourCoffee(); Console.WriteLine("coffee is ready"); Egg eggs = FryEggs(2); Console.WriteLine("eggs are ready"); Bacon bacon = FryBacon(3); Console.WriteLine("bacon is ready"); Toast toast = ToastBread(2); ApplyButter(toast); ApplyJam(toast); Console.WriteLine("toast is ready"); Juice oj = PourOJ(); Console.WriteLine("oj is ready"); Console.WriteLine("Breakfast is ready!"); Console.WriteLine($"totol time:{sw.ElapsedMilliseconds/1000}"); Console.ReadKey(); } private static Juice PourOJ() { Console.WriteLine("Pouring orange juice"); return new Juice(); } private static void ApplyJam(Toast toast) => Console.WriteLine("Putting jam on the toast"); private static void ApplyButter(Toast toast) => Console.WriteLine("Putting butter on the toast"); private static Toast ToastBread(int slices) { for (int slice = 0; slice < slices; slice++) { Console.WriteLine("Putting a slice of bread in the toaster"); } Console.WriteLine("Start toasting..."); Task.Delay(3000).Wait(); Console.WriteLine("Remove toast from toaster"); return new Toast(); } private static Bacon FryBacon(int slices) { Console.WriteLine($"putting {slices} slices of bacon in the pan"); Console.WriteLine("cooking first side of bacon..."); Task.Delay(3000).Wait(); for (int slice = 0; slice < slices; slice++) { Console.WriteLine("flipping a slice of bacon"); } Console.WriteLine("cooking the second side of bacon..."); Task.Delay(3000).Wait(); Console.WriteLine("Put bacon on plate"); return new Bacon(); } private static Egg FryEggs(int howMany) { Console.WriteLine("Warming the egg pan..."); Task.Delay(3000).Wait(); Console.WriteLine($"cracking {howMany} eggs"); Console.WriteLine("cooking the eggs ..."); Task.Delay(3000).Wait(); Console.WriteLine("Put eggs on plate"); return new Egg(); } private static Coffee PourCoffee() { Console.WriteLine("Pouring coffee"); return new Coffee(); } } class Coffee { } class Egg { } class Bacon { } class Toast { } class Juice { } }
執行效果如下:
或表示為這樣
同步準備的早餐大約花費了 30 分鐘,因為總耗時是每個任務耗時的總和。這裡的total time只是用來表示記錄下程式執行的時間。
而我以前寫的非同步程式碼是這樣的:
using System; using System.Diagnostics; using System.Threading.Tasks; namespace AsyncBreakfast { class Program { static async void Main(string[] args) { var sw = new Stopwatch(); sw.Start(); Coffee cup = PourCoffee(); Console.WriteLine("coffee is ready"); Egg eggs = await FryEggsAsync(2); Console.WriteLine("eggs are ready"); Bacon bacon = await FryBaconAsync(3); Console.WriteLine("bacon is ready"); Toast toast = await ToastBreadAsync(2); ApplyButter(toast); ApplyJam(toast); Console.WriteLine("toast is ready"); Juice oj = PourOJ(); Console.WriteLine("oj is ready"); Console.WriteLine("Breakfast is ready!"); Console.WriteLine($"totol time:{sw.ElapsedMilliseconds/1000}"); Console.ReadKey(); } static async Task<Toast> MakeToastWithButterAndJamAsync(int number) { var toast = await ToastBreadAsync(number); ApplyButter(toast); ApplyJam(toast); return toast; } private static Juice PourOJ() { Console.WriteLine("Pouring orange juice"); return new Juice(); } private static void ApplyJam(Toast toast) => Console.WriteLine("Putting jam on the toast"); private static void ApplyButter(Toast toast) => Console.WriteLine("Putting butter on the toast"); private static async Task<Toast> ToastBreadAsync(int slices) { for (int slice = 0; slice < slices; slice++) { Console.WriteLine("Putting a slice of bread in the toaster"); } Console.WriteLine("Start toasting..."); await Task.Delay(3000); Console.WriteLine("Remove toast from toaster"); return new Toast(); } private static async Task<Bacon> FryBaconAsync(int slices) { Console.WriteLine($"putting {slices} slices of bacon in the pan"); Console.WriteLine("cooking first side of bacon..."); await Task.Delay(3000); for (int slice = 0; slice < slices; slice++) { Console.WriteLine("flipping a slice of bacon"); } Console.WriteLine("cooking the second side of bacon..."); await Task.Delay(3000); Console.WriteLine("Put bacon on plate"); return new Bacon(); } private static async Task<Egg> FryEggsAsync(int howMany) { Console.WriteLine("Warming the egg pan..."); await Task.Delay(3000); Console.WriteLine($"cracking {howMany} eggs"); Console.WriteLine("cooking the eggs ..."); await Task.Delay(3000); Console.WriteLine("Put eggs on plate"); return new Egg(); } private static Coffee PourCoffee() { Console.WriteLine("Pouring coffee"); return new Coffee(); } } class Coffee { } class Egg { } class Bacon { } class Toast { } class Juice { } }
效果如下:
可以看出,這樣編寫的非同步和最初同步版本的總共的耗時大致相同。
這是因為這段程式碼還沒有利用非同步程式設計的某些關鍵功能。
即上面的非同步程式碼的使用在這裡是不準確的。
可以看出,這段程式碼裡面的列印輸出與同步是一樣的。
這是因為:在煎雞蛋或培根時,此程式碼雖然不會阻塞,但是此程式碼也不會啟動任何其他任務。
就造成了非同步煎雞蛋的操作完成後,才會開始培根製作。
但是,對於這裡而言,我不希望每個任務都按順序依次執行。
最好是首先啟動每個元件任務,然後再等待之前任務的完成。
例如:首先啟動雞蛋和培根。
同時啟動任務
在很多方案中,你可能都希望立即啟動若干獨立的任務。然後,在每個任務完成時,你可以繼續
進行已經準備的其他工作。
就像這裡同時啟動煎雞蛋,培根和烤麵包。
我們這裡對早餐程式碼做些更改。
正確的做法
第一步是儲存任務以便在這些任務啟動時進行操作,而不是等待:
Coffee cup = PourCoffee(); Console.WriteLine("coffee is ready"); Task<Egg> eggsTask = FryEggsAsync(2); Egg eggs = await eggsTask; Console.WriteLine("eggs are ready"); Task<Bacon> baconTask = FryBaconAsync(3); Bacon bacon = await baconTask; Console.WriteLine("bacon is ready"); Task<Toast> toastTask = ToastBreadAsync(2); Toast toast = await toastTask; ApplyButter(toast); ApplyJam(toast); Console.WriteLine("toast is ready"); Juice oj = PourOJ(); Console.WriteLine("oj is ready"); Console.WriteLine("Breakfast is ready!");
接下來,可以在提供早餐之前將用於處理培根和雞蛋的await語句移動到此方法的末尾:
Coffee cup = PourCoffee(); Console.WriteLine("coffee is ready"); Task<Egg> eggsTask = FryEggsAsync(2); Task<Bacon> baconTask = FryBaconAsync(3); Task<Toast> toastTask = ToastBreadAsync(2); Toast toast = await toastTask; ApplyButter(toast); ApplyJam(toast); Console.WriteLine("toast is ready"); Juice oj = PourOJ(); Console.WriteLine("oj is ready"); Egg eggs = await eggsTask; Console.WriteLine("eggs are ready"); Bacon bacon = await baconTask; Console.WriteLine("bacon is ready"); Console.WriteLine("Breakfast is ready!");
執行效果如下:
或者
可以看出,這裡一次啟動了所有的非同步任務。而你僅在需要結果時,才會等待每項任務。
這裡非同步準備的造成大約花費20分鐘,這是因為一些任務可以併發進行。
而對於直接 Egg eggs = await FryEggsAsync(2); 的方式,適用於你只需要等待這一個非同步操作結果,不需要進行其他操作的時候。
與任務組合
吐司操作由非同步操作(烤麵包)和同步操作(新增黃油和果醬)組成。
這裡涉及到一個重要概念:
非同步操作後跟同步操作的這種組合也是一個非同步操作。
也就是說,如果操作的任何部分是非同步的,整個操作就是非同步的。
程式碼如下:
static async Task<Toast> MakeToastWithButterAndJamAsync(int number) { var toast = await ToastBreadAsync(number); ApplyButter(toast); ApplyJam(toast); return toast; }
所有,主要程式碼塊現在變為:
static async Task Main(string[] args) { Coffee cup = PourCoffee(); Console.WriteLine("coffee is ready"); var eggsTask = FryEggsAsync(2); var baconTask = FryBaconAsync(3); var toastTask = MakeToastWithButterAndJamAsync(2); var eggs = await eggsTask; Console.WriteLine("eggs are ready"); var bacon = await baconTask; Console.WriteLine("bacon is ready"); var toast = await toastTask; Console.WriteLine("toast is ready"); Juice oj = PourOJ(); Console.WriteLine("oj is ready"); Console.WriteLine("Breakfast is ready!"); }
高效的等待任務
可以通過使用Task類的方法改進上述程式碼末尾一系列await語句。
WhenAll 是其中的一個api , 它將返回一個其引數列表中的所有任務都已完成時猜完成的Task,
程式碼如下
await Task.WhenAll(eggsTask, baconTask, toastTask); Console.WriteLine("eggs are ready"); Console.WriteLine("bacon is ready"); Console.WriteLine("toast is ready"); Console.WriteLine("Breakfast is ready!");
另一種選擇是 WhenAny, 它將返回一個,當其引數完成時猜完成的 Task<Task>。
var breakfastTasks = new List<Task> { eggsTask, baconTask, toastTask }; while (breakfastTasks.Count > 0) { Task finishedTask = await Task.WhenAny(breakfastTasks); if (finishedTask == eggsTask) { Console.WriteLine("eggs are ready"); } else if (finishedTask == baconTask) { Console.WriteLine("bacon is ready"); } else if (finishedTask == toastTask) { Console.WriteLine("toast is ready"); } breakfastTasks.Remove(finishedTask); }
處理已完成任務的結果之後,可以從傳遞給 WhenAny
的任務列表中刪除此已完成的任務。
進行這些更改後,程式碼的最終版本將如下所示:
using System; using System.Collections.Generic; using System.Threading.Tasks; namespace AsyncBreakfast { class Program { static async Task Main(string[] args) { Coffee cup = PourCoffee(); Console.WriteLine("coffee is ready"); var eggsTask = FryEggsAsync(2); var baconTask = FryBaconAsync(3); var toastTask = MakeToastWithButterAndJamAsync(2); var breakfastTasks = new List<Task> { eggsTask, baconTask, toastTask }; while (breakfastTasks.Count > 0) { Task finishedTask = await Task.WhenAny(breakfastTasks); if (finishedTask == eggsTask) { Console.WriteLine("eggs are ready"); } else if (finishedTask == baconTask) { Console.WriteLine("bacon is ready"); } else if (finishedTask == toastTask) { Console.WriteLine("toast is ready"); } breakfastTasks.Remove(finishedTask); } Juice oj = PourOJ(); Console.WriteLine("oj is ready"); Console.WriteLine("Breakfast is ready!"); } static async Task<Toast> MakeToastWithButterAndJamAsync(int number) { var toast = await ToastBreadAsync(number); ApplyButter(toast); ApplyJam(toast); return toast; } private static Juice PourOJ() { Console.WriteLine("Pouring orange juice"); return new Juice(); } private static void ApplyJam(Toast toast) => Console.WriteLine("Putting jam on the toast"); private static void ApplyButter(Toast toast) => Console.WriteLine("Putting butter on the toast"); private static async Task<Toast> ToastBreadAsync(int slices) { for (int slice = 0; slice < slices; slice++) { Console.WriteLine("Putting a slice of bread in the toaster"); } Console.WriteLine("Start toasting..."); await Task.Delay(3000); Console.WriteLine("Remove toast from toaster"); return new Toast(); } private static async Task<Bacon> FryBaconAsync(int slices) { Console.WriteLine($"putting {slices} slices of bacon in the pan"); Console.WriteLine("cooking first side of bacon..."); await Task.Delay(3000); for (int slice = 0; slice < slices; slice++) { Console.WriteLine("flipping a slice of bacon"); } Console.WriteLine("cooking the second side of bacon..."); await Task.Delay(3000); Console.WriteLine("Put bacon on plate"); return new Bacon(); } private static async Task<Egg> FryEggsAsync(int howMany) { Console.WriteLine("Warming the egg pan..."); await Task.Delay(3000); Console.WriteLine($"cracking {howMany} eggs"); Console.WriteLine("cooking the eggs ..."); await Task.Delay(3000); Console.WriteLine("Put eggs on plate"); return new Egg(); } private static Coffee PourCoffee() { Console.WriteLine("Pouring coffee"); return new Coffee(); } } }
效果如下:
或者
這種非同步的程式碼實現最終大約花費15分鐘,因為一些任務能同時執行,
並且該程式碼能夠同時監視多個任務,只在需要時才執行操作。
總結:
async 和 await的功能最好能做到:
儘可能啟動任務,不要在等待任務完成時造成阻塞。
即可以先把任務儲存到task,然後在後面需要用的時候,呼叫await task()方法。
參考網址:https://docs.microsoft.com/zh-cn/dotnet/csharp/programming-guide/concepts/async/