关于async和await的探讨
緣起
最近在看《深入解析C#(第4版)》這本書,看到了第五章,這一章節是關于異步。之前對異步這個概念只能算是一知半解,了解了它的概念和用法,但是對它的實際場景和為了解決什么問題而誕生的是不太清楚的。于是乎,就和小伙伴之間有了一場討論。
概念
一般來說對方法的調用都是同步執行的。例如在線程執行體內,即線程的調用函數中,方法的調用就是同步執行的。如果方法需要很長的時間來完成,比方說從Internet加載數據的方法,調用者線程將被阻塞直到方法調用完成。這時候為了避免調用者線程被阻塞,這時候就需要用到異步編程了。異步編程可以解決線程因為等待獨占式任務而導致的阻塞問題。
探索
探索過程中,參考了《微軟官方文檔》,《I/O Threads Explained》。
例子說明
官方以一個做早餐的例子來解釋了什么叫同步,并行和異步。
假設做一個早餐需要完成7個步驟:
倒一杯咖啡。
加熱平底鍋,然后煎兩個雞蛋。
煎三片培根。
烤兩片面包。
在烤面包上加黃油和果醬。
倒一杯橙汁。
同步執行
同步執行,是指只有完成上一個任務,才會開始下一個任務;同時將阻塞當前線程執行其他操作,直至任務全部完成
代碼例子如下:
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 | // 鑒于我用的是vs2022,可能控制臺程序的代碼在舊版本的vs上無法直接運行,需要補充對應的main函數 MakeBreakfast();static void MakeBreakfast() {var cup = PourCoffee();Console.WriteLine("coffee is ready");var eggs = FryEggs(2);Console.WriteLine("eggs are ready");var bacon = FryBacon(3);Console.WriteLine("bacon is ready");var toast = ToastBread(2);ApplyButter(toast);ApplyJam(toast);Console.WriteLine("toast is ready");var oj = PourOJ();Console.WriteLine("oj is ready");Console.WriteLine("Breakfast is ready!"); }static Juice PourOJ() {Console.WriteLine("Pouring orange juice");return new Juice(); }static void ApplyJam(Toast toast) => Console.WriteLine("Putting jam on the toast");static void ApplyButter(Toast toast) =>Console.WriteLine("Putting butter on the toast");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(); }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(); }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(); }static Coffee PourCoffee() {Console.WriteLine("Pouring coffee");return new Coffee(); }public class Juice { }public class Bacon { }public class Egg { }public class Coffee { }public class Toast { } |
同步執行的總耗時是每個任務耗時的總和。此外,因為是同步執行的原因,在開始制作一份早餐的時候,如果此時又有一份制作早餐的請求過來,是不會開始制作的。如果是客戶端程序,使用同步執行耗時時間長的操作,會導致UI線程被阻塞,導致UI線程無法響應用戶操作,直至操作完成后,UI線程才相應用戶的操作。
異步執行
異步執行,是指在遇到await的時候,才需要等待異步操作完成,然后往下執行;但是不會阻塞當前線程執行其他操作。
代碼如下
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 | await MakeBreakfastAsync();static async Task MakeBreakfastAsync() {var cup = PourCoffee();Console.WriteLine("coffee is ready");var eggs = await FryEggsAsync(2);Console.WriteLine("eggs are ready");var bacon = await FryBaconAsync(3);Console.WriteLine("bacon is ready");var toast = await ToastBreadAsync(2);ApplyButter(toast);ApplyJam(toast);Console.WriteLine("toast is ready");var oj = PourOJ();Console.WriteLine("oj is ready");Console.WriteLine("Breakfast is ready!"); }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...");Task.Delay(3000).Wait();Console.WriteLine("Remove toast from toaster");return await Task.FromResult(new Toast()); }static Task<Bacon> FryBaconAsync(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 Task.FromResult(new Bacon()); }static Task<Egg> FryEggsAsync(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 Task.FromResult(new Egg()); } |
上面代碼只是為了避免堵塞當前的線程,并沒有真正用上異步執行的某些關鍵功能,所以在耗時上是相差不遠的;但是這時候如果在接受了一份制作早餐的請求,還未完成的時候,又有一份制作早餐的請求過來,是可能會開始制作另一份早餐的。
改善后的異步執行
代碼如下
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 | await MakeBreakfastBetterAsync();static async Task MakeBreakfastBetterAsync() {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!"); } |
異步方法的邏輯沒有改變,只是調整了一下代碼的執行順序,一開始就調用了三個異步方法,只是在await語句后置了,而不是上面那段代碼一樣,執行了就在那里等待任務完成,而是會去進行其他的后續操作,直至后續操作需要用到前面任務執行結果的時候,才去獲取對應的執行結果,如果沒有執行完成就等待執行完成才繼續后續的操作。
異步執行并不總是需要另一個線程來執行新任務。并行編程是異步執行的一個子集。
并行編程
并行編程,調用多個線程,同時去執行任務
例如:需要制作五份早餐,同步和異步的方法都是需要循環調用相應的MakeBreakfast方法和MakeBreakfastBetterAsync方法五次才能制作完成。而并行編程,也就是多線程,可以一次性創建五個線程,分別制作一份早餐,從而大大縮短了所需要的時間。
代碼如下
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 | DateTime beforeDT = DateTime.Now; for (int i = 0; i < 5; i++) {MakeBreakfast(); } DateTime afterDT = DateTime.Now; TimeSpan ts = afterDT.Subtract(beforeDT); Console.WriteLine($"同步執行程序耗時: {ts.TotalMilliseconds}ms");beforeDT = DateTime.Now; for (int i = 0; i < 5; i++) {await MakeBreakfastBetterAsync(); } afterDT = DateTime.Now; ts = afterDT.Subtract(beforeDT); Console.WriteLine($"異步執行程序耗時: {ts.TotalMilliseconds}ms");beforeDT = DateTime.Now; await MakeBreakfastBetterMultiTask(); afterDT = DateTime.Now; ts = afterDT.Subtract(beforeDT); Console.WriteLine($"并行編程程序耗時: {ts.TotalMilliseconds}ms");static async Task MakeBreakfastBetterMultiTask() {Task[] tasks = new Task[5];for (int i = 0; i < 5; i++){tasks[i] = new Task((parameter) => MakeBreakfastBetterAsync().Wait(), "aaa");tasks[i].Start();}Task.WaitAll(tasks); } |
運行耗時結果如下
相比之下,顯然能看出來之間的運行耗時差別還是有點大的。
一個通俗的例子
程序就像一個餐館,線程就像餐館里面已有的廚師,CPU就是調度廚師的廚師長,假設餐館開業了,廚師長只帶了5個廚師,餐館接到的訂單有8份,同步執行就是5個廚師分別處理5個訂單后,這期間,他們會專心的去完成訂單的菜,而無視其他的事情,直到完成訂單,廚師長才會分配新的訂單給他們;異步執行則是5個廚師在處理5個訂單的期間,如果廚師長發現他們有人處于空閑狀態,就會安排他們去執行剩下3個訂單,如果收到等待中的訂單可以繼續操作時,廚師長會抽調廚師繼續完成訂單,從而增加了餐館處理訂單的能力。而并行編程則是餐館開業的時候,告訴了廚師長,需要8個廚師;廚師長就帶來了相應數量的廚師來處理訂單。
這就是這兩天,我對同步,異步和并行之間的感悟。如有不對,敬請指正!
推薦閱讀:
《Kubernetes全棧架構師(Kubeadm高可用安裝k8s集群)--學習筆記》
《.NET 云原生架構師訓練營(模塊一 架構師與云原生)--學習筆記》
《.NET Core開發實戰(第1課:課程介紹)--學習筆記》
總結
以上是生活随笔為你收集整理的关于async和await的探讨的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 有关[Http持久连接]的一切,卷给你看
- 下一篇: 验证规则构建神器 FluentValid