C# 通過同步和異步實現優化做早餐的時間

概述

一天之計在於晨,每天的早餐也是必不可少,但是很多人為瞭節約時間,都是簡單的吃點湊合一下或幹脆不吃早餐,這對於個人身體和工作效率來說,無疑是不合理的,那麼要如何做一頓早餐呢?如何能節約做早餐的時間呢?本文以一個簡單的小例子,簡述如何做一頓早餐及如何優化做早餐的時間。僅供學習分享使用,如有不足之處,還請指正。

正常情況下,做早餐可以分為以下幾個步驟:

  1. 倒一杯咖啡。
  2. 加熱平底鍋,然後煎兩個雞蛋。
  3. 煎三片培根。
  4. 烤兩片面包。
  5. 在烤面包上加黃油和果醬。
  6. 倒一杯橙汁。

同步方式做早餐

根據以上步驟進行編程,做一份早餐需要編寫程序如下:

/// <summary>
        /// 同步做早餐
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void btnBreakfast_Click(object sender, EventArgs e)
        {
            this.txtInfo.Clear();
            Stopwatch watch = Stopwatch.StartNew();
            watch.Start();
            //1. 倒一杯咖啡。
            string cup = PourCoffee();
            PrintInfo("咖啡沖好瞭");
            //2. 加熱平底鍋,然後煎兩個雞蛋。
            string eggs = FryEggs(2);
            PrintInfo("雞蛋煎好瞭");
            //3. 煎三片培根。
            string bacon = FryBacon(3);
            PrintInfo("培根煎好瞭");
            //4. 烤兩片面包。
            string toast = ToastBread(2);
            //5. 在烤面包上加黃油和果醬。
            ApplyButter(toast);
            ApplyJam(toast);
            PrintInfo("面包烤好瞭");
            //6. 倒一杯橙汁。
            string oj = PourOJ();
            PrintInfo("橙汁倒好瞭");
            PrintInfo("早餐準備完畢!");
            watch.Stop();
            TimeSpan time = watch.Elapsed;
            PrintInfo(string.Format("總運行時間為:{0}秒", time.TotalSeconds.ToString("0.00")));
        }

        /// <summary>
        /// 倒一杯咖啡
        /// </summary>
        /// <returns></returns>
        private string PourCoffee()
        {
            PrintInfo("正在沖咖啡...");
            return "咖啡";
        }

        /// <summary>
        /// 抹果醬
        /// </summary>
        /// <param name="toast"></param>
        private void ApplyJam(string toast) =>
            PrintInfo("往面包抹果醬");

        /// <summary>
        /// 抹黃油
        /// </summary>
        /// <param name="toast"></param>
        private void ApplyButter(string toast) =>
            PrintInfo("往面包抹黃油");

        /// <summary>
        /// 烤面包
        /// </summary>
        /// <param name="slices"></param>
        /// <returns></returns>
        private string ToastBread(int slices)
        {
            for (int slice = 0; slice < slices; slice++)
            {
                PrintInfo("往烤箱裡面放面包");
            }
            PrintInfo("開始烤...");
            Task.Delay(3000).Wait();
            PrintInfo("從烤箱取出面包");

            return "烤面包";
        }

        /// <summary>
        /// 煎培根
        /// </summary>
        /// <param name="slices"></param>
        /// <returns></returns>
        private string FryBacon(int slices)
        {
            PrintInfo($"放 {slices} 片培根在平底鍋");
            PrintInfo("煎第一片培根...");
            Task.Delay(3000).Wait();
            for (int slice = 0; slice < slices; slice++)
            {
                PrintInfo("翻轉培根");
            }
            PrintInfo("煎第二片培根...");
            Task.Delay(3000).Wait();
            PrintInfo("把培根放盤子裡");

            return "煎培根";
        }

        /// <summary>
        /// 煎雞蛋
        /// </summary>
        /// <param name="howMany"></param>
        /// <returns></returns>
        private string FryEggs(int howMany)
        {
            PrintInfo("加熱平底鍋...");
            Task.Delay(3000).Wait();
            PrintInfo($"磕開 {howMany} 個雞蛋");
            PrintInfo("煎雞蛋 ...");
            Task.Delay(3000).Wait();
            PrintInfo("雞蛋放盤子裡");

            return "煎雞蛋";
        }

        /// <summary>
        /// 倒橙汁
        /// </summary>
        /// <returns></returns>
        private string PourOJ()
        {
            PrintInfo("倒一杯橙汁");
            return "橙汁";
        }

同步做早餐示例

通過運行示例,發現采用同步方式進行編程,做一份早餐,共計15秒鐘,且在此15秒鐘時間內,程序處於【卡住】狀態,無法進行其他操作。如下所示:

同步做早餐示意圖

同步方式做早餐,就是一個做完,再進行下一個,順序執行,如下所示:

同步方式為何會【卡住】?

因為在程序進程中,會有一個主線程,用於響應用戶的操作,同步方式下,做早餐的和前端頁面同在主線程中,所以當開始做早餐時,就不能響應其他的操作瞭。這就是【兩耳不聞窗外事,一心隻讀聖賢書】的境界。但如果讓用戶長時間處於等待狀態,會讓用戶體驗很不友好。比如,劉玄德三顧茅廬,大雪紛飛之下,諸葛亮在草廬中午睡,劉關張在大雪中靜等。試問有幾人會有玄德的耐心,何況程序也不是諸葛亮,用戶也沒有玄德的耐心!

異步方式做早餐

上述代碼演示瞭不正確的實踐:構造同步代碼來執行異步操作。 顧名思義,此代碼將阻止執行這段代碼的線程執行任何其他操作。 在任何任務進行過程中,此代碼也不會被中斷。 就如同你將面包放進烤面包機後盯著此烤面包機一樣。 你會無視任何跟你說話的人,直到面包彈出。如何做才能避免線程阻塞呢?答案就是異步。 await 關鍵字提供瞭一種非阻塞方式來啟動任務,然後在此任務完成時繼續執行。

首先更新代碼,對於耗時的程序,采用異步方式做早餐,如下所示:

private async void btnBreakfastAsync_Click(object sender, EventArgs e)
        {
            this.txtInfo.Clear();
            Stopwatch watch = Stopwatch.StartNew();
            watch.Start();
            //1. 倒一杯咖啡。
            string cup = PourCoffee();
            PrintInfo("咖啡沖好瞭");
            //2. 加熱平底鍋,然後煎兩個雞蛋。
            //Task<string> eggs = FryEggsAsync(2);
            string eggs =await FryEggsAsync(2);
            PrintInfo("雞蛋煎好瞭");
            //3. 煎三片培根。
            string bacon =await FryBaconAsync(3);
            PrintInfo("培根煎好瞭");
            //4. 烤兩片面包。
            string toast =await ToastBreadAsync(2);
            //5. 在烤面包上加黃油和果醬。
            ApplyButter(toast);
            ApplyJam(toast);
            PrintInfo("面包烤好瞭");
            //6. 倒一杯橙汁。
            string oj = PourOJ();
            PrintInfo("橙汁倒好瞭");
            PrintInfo("早餐準備完畢!");
            watch.Stop();
            TimeSpan time = watch.Elapsed;
            PrintInfo(string.Format("總運行時間為:{0}秒", time.TotalSeconds.ToString("0.00")));
        }

        /// <summary>
        /// 異步烤面包
        /// </summary>
        /// <param name="slices"></param>
        /// <returns></returns>
        private async Task<string> ToastBreadAsync(int slices)
        {
            for (int slice = 0; slice < slices; slice++)
            {
                PrintInfo("往烤箱裡面放面包");
            }
            PrintInfo("開始烤...");
            await Task.Delay(3000);
            PrintInfo("從烤箱取出面包");

            return "烤面包";
        }

        /// <summary>
        /// 異步煎培根
        /// </summary>
        /// <param name="slices"></param>
        /// <returns></returns>
        private async Task<string> FryBaconAsync(int slices)
        {
            PrintInfo($"放 {slices} 片培根在平底鍋");
            PrintInfo("煎第一片培根...");
            await Task.Delay(3000);
            for (int slice = 0; slice < slices; slice++)
            {
                PrintInfo("翻轉培根");
            }
            PrintInfo("煎第二片培根...");
            await Task.Delay(3000);
            PrintInfo("把培根放盤子裡");

            return "煎培根";
        }

        /// <summary>
        /// 異步煎雞蛋
        /// </summary>
        /// <param name="howMany"></param>
        /// <returns></returns>
        private async Task<string> FryEggsAsync(int howMany)
        {
            PrintInfo("加熱平底鍋...");
            await Task.Delay(3000);
            PrintInfo($"磕開 {howMany} 個雞蛋");
            PrintInfo("煎雞蛋 ...");
            await Task.Delay(3000);
            PrintInfo("雞蛋放盤子裡");

            return "煎雞蛋";
        }

註意:通過測試發現,異步方式和同步方式的執行時間一致,所以采用異步方式並不會縮短時間,但是程序已不再阻塞,可以同時響應用戶的其他請求。

優化異步做早餐

通過上述異步方式,雖然優化瞭程序,不再阻塞,但是時間並沒有縮短,那麼要如何優化程序來縮短時間,以便早早的吃上可口的早餐呢?答案就是在開始一個任務後,在等待任務完成時,可以繼續進行準備其他的任務。 你也幾乎將在同一時間完成所有工作。 你將吃到一頓熱氣騰騰的早餐。通過合並任務和調整任務的順序,將大大節約任務的完成時間,如下所示:

/// <summary>
        /// 優化異步做早餐
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private async void btnBreakfast2_Click(object sender, EventArgs e)
        {
            this.txtInfo.Clear();
            Stopwatch watch = Stopwatch.StartNew();
            watch.Start();
            //1. 倒一杯咖啡。
            string cup = PourCoffee();
            PrintInfo("咖啡沖好瞭");
            //2. 加熱平底鍋,然後煎兩個雞蛋。
            Task<string> eggsTask = FryEggsAsync(2);
            //3. 煎三片培根。
            Task<string> baconTask = FryBaconAsync(3);
            //4.5合起來 烤面包,抹果醬,黃油
            Task<string> toastTask = MakeToastWithButterAndJamAsync(2);

            string eggs = await eggsTask;
            PrintInfo("雞蛋煎好瞭");

            string bacon = await baconTask;
            PrintInfo("培根煎好瞭");

            string toast = await toastTask;
            PrintInfo("面包烤好瞭");
            //6. 倒一杯橙汁。
            string oj = PourOJ();
            PrintInfo("橙汁倒好瞭");
            PrintInfo("早餐準備完畢!");
            watch.Stop();
            TimeSpan time = watch.Elapsed;
            PrintInfo(string.Format("總運行時間為:{0}秒", time.TotalSeconds.ToString("0.00")));
        }

        /// <summary>
        /// 組合任務
        /// </summary>
        /// <param name="number"></param>
        /// <returns></returns>
        private async Task<string> MakeToastWithButterAndJamAsync(int number)
        {
            var toast = await ToastBreadAsync(number);
            ApplyButter(toast);
            ApplyJam(toast);
            return toast;
        }

在本例中,合並瞭【烤面包+抹果醬+抹黃油】為一個任務,這樣是烤面包的同時,可以煎雞蛋,煎培根,三項耗時任務同時執行。在三個任務都完成是,早餐也就做好瞭,示例如下所示:

通過以上優化示例發現,通過合並任務和調整順序,做一份早餐,需要6.06秒。

優化異步早餐示意圖

優化後的異步做早餐,由於一些任務並發運行,因此節約瞭時間。示意圖如下所示:

異步異常

上述示例假定所有的任務都可以正常完成,那麼如果某一個任務執行過程中發生瞭異常,要如何捕獲呢?答案是:當任務無法成功完成時,它們將引發異常。 當啟動的任務為 awaited 時,客戶端代碼可捕獲這些異常。

例如當烤面包的時候,烤箱突然著火瞭,如何處理異常呢?代碼如下所示:

private async void btnBreakfastAsync3_Click(object sender, EventArgs e)
        {
            try
            {
                this.txtInfo.Clear();
                Stopwatch watch = Stopwatch.StartNew();
                watch.Start();
                //1. 倒一杯咖啡。
                string cup = PourCoffee();
                PrintInfo("咖啡沖好瞭");
                //2. 加熱平底鍋,然後煎兩個雞蛋。
                Task<string> eggsTask = FryEggsAsync(2);
                //3. 煎三片培根。
                Task<string> baconTask = FryBaconAsync(3);
                //4.5合起來 烤面包,抹果醬,黃油
                Task<string> toastTask = MakeToastWithButterAndJamAsyncEx(2);

                string eggs = await eggsTask;
                PrintInfo("雞蛋煎好瞭");

                string bacon = await baconTask;
                PrintInfo("培根煎好瞭");

                string toast = await toastTask;
                PrintInfo("面包烤好瞭");
                //6. 倒一杯橙汁。
                string oj = PourOJ();
                PrintInfo("橙汁倒好瞭");
                PrintInfo("早餐準備完畢!");
                watch.Stop();
                TimeSpan time = watch.Elapsed;
                PrintInfo(string.Format("總運行時間為:{0}秒", time.TotalSeconds.ToString("0.00")));
            }
            catch (AggregateException ex) {
                PrintInfo("線程內部異常");
                PrintInfo(ex.StackTrace);
            }
            catch (Exception ex)
            {
                PrintInfo("其他異常");
                PrintInfo(ex.Message);
            }
        }

        /// <summary>
        /// 組合任務
        /// </summary>
        /// <param name="number"></param>
        /// <returns></returns>
        private async Task<string> MakeToastWithButterAndJamAsyncEx(int number)
        {
            var toast = await ToastBreadAsyncEx(number);
            ApplyButter(toast);
            ApplyJam(toast);
            return toast;
        }

        /// <summary>
        /// 異步烤面包異常
        /// </summary>
        /// <param name="slices"></param>
        /// <returns></returns>
        private async Task<string> ToastBreadAsyncEx(int slices)
        {
            for (int slice = 0; slice < slices; slice++)
            {
                PrintInfo("往烤箱裡面放面包");
            }
            PrintInfo("開始烤...");
            await Task.Delay(2000);
            PrintInfo("著火瞭! 面包糊瞭!");
            int a = 1, b = 0;
            int i = a / b;//制造一個異常
            //throw new InvalidOperationException("烤箱著火瞭!");
            await Task.Delay(1000);
            PrintInfo("從烤箱取出面包");

            return "烤面包";
        }

異步任務異常示例

請註意,從烤面包機著火到發現異常,有相當多的任務要完成。 當異步運行的任務引發異常時,該任務出錯。 Task 對象包含 Task.Exception 屬性中引發的異常。 出錯的任務在等待時引發異常。

需要理解兩個重要機制:異常在出錯的任務中的存儲方式,以及在代碼等待出錯的任務時解包並重新引發異常的方式。

當異步運行的代碼引發異常時,該異常存儲在 Task 中。 Task.Exception 屬性為 System.AggregateException,因為異步工作期間可能會引發多個異常。 引發的任何異常都將添加到 AggregateException.InnerExceptions 集合中。 如果該 Exception 屬性為 NULL,則將創建一個新的 AggregateException 且引發的異常是該集合中的第一項。

對於出錯的任務,最常見的情況是 Exception 屬性隻包含一個異常。 當代碼 awaits 出錯的任務時,將重新引發 AggregateException.InnerExceptions 集合中的第一個異常。 因此,此示例的輸出顯示 InvalidOperationException 而不是 AggregateException。 提取第一個內部異常使得使用異步方法與使用其對應的同步方法盡可能相似。 當你的場景可能生成多個異常時,可在代碼中檢查 Exception 屬性。

高效的等待

通過以上示例,需要等待很多任務完成,然後早餐才算做好,那麼如何才能高效優雅的等待呢?可以通過使用 Task 類的方法改進上述代碼末尾的一系列 await 語句。其中一個 API 是 WhenAll,它將返回一個其參數列表中的所有任務都已完成時才完成的 Task,如下所示:

private async void btnBreakfastAsync4_Click(object sender, EventArgs e)
        {
            this.txtInfo.Clear();
            Stopwatch watch = Stopwatch.StartNew();
            watch.Start();
            //1. 倒一杯咖啡。
            string cup = PourCoffee();
            PrintInfo("咖啡沖好瞭");
            //2. 加熱平底鍋,然後煎兩個雞蛋。
            Task<string> eggsTask = FryEggsAsync(2);
            //3. 煎三片培根。
            Task<string> baconTask = FryBaconAsync(3);
            //4.5合起來 烤面包,抹果醬,黃油
            Task<string> toastTask = MakeToastWithButterAndJamAsync(2);
            //等待任務完成
            await Task.WhenAll(eggsTask, baconTask, toastTask);

            PrintInfo("雞蛋煎好瞭");
            PrintInfo("培根煎好瞭");
            PrintInfo("面包烤好瞭");
            //6. 倒一杯橙汁。
            string oj = PourOJ();
            PrintInfo("橙汁倒好瞭");
            PrintInfo("早餐準備完畢!");
            watch.Stop();
            TimeSpan time = watch.Elapsed;
            PrintInfo(string.Format("總運行時間為:{0}秒", time.TotalSeconds.ToString("0.00")));
        }

另一種選擇是使用 WhenAny,它將返回一個當其參數完成時才完成的 Task<Task>。如下所示:

private async void btnBreakfastAsync5_Click(object sender, EventArgs e)
        {
            this.txtInfo.Clear();
            Stopwatch watch = Stopwatch.StartNew();
            watch.Start();
            //1. 倒一杯咖啡。
            string cup = PourCoffee();
            PrintInfo("咖啡沖好瞭");
            //2. 加熱平底鍋,然後煎兩個雞蛋。
            Task<string> eggsTask = FryEggsAsync(2);
            //3. 煎三片培根。
            Task<string> baconTask = FryBaconAsync(3);
            //4.5合起來 烤面包,抹果醬,黃油
            Task<string> toastTask = MakeToastWithButterAndJamAsync(2);
            //等待任務完成
            var breakfastTasks = new List<Task> { eggsTask, baconTask, toastTask };
            while (breakfastTasks.Count > 0)
            {
                Task finishedTask = await Task.WhenAny(breakfastTasks);
                if (finishedTask == eggsTask)
                {
                    PrintInfo("雞蛋煎好瞭");
                }
                else if (finishedTask == baconTask)
                {
                    PrintInfo("培根煎好瞭");
                }
                else if (finishedTask == toastTask)
                {
                    PrintInfo("面包烤好瞭");
                }
                breakfastTasks.Remove(finishedTask);
            }
            //6. 倒一杯橙汁。
            string oj = PourOJ();
            PrintInfo("橙汁倒好瞭");
            PrintInfo("早餐準備完畢!");
            watch.Stop();
            TimeSpan time = watch.Elapsed;
            PrintInfo(string.Format("總運行時間為:{0}秒", time.TotalSeconds.ToString("0.00")));
        }

這就是由同步到異步再到優化異步任務的逐步過程

以上就是C# 通過同步和異步實現優化做早餐的時間的詳細內容,更多關於C# 同步 異步的資料請關註WalkonNet其它相關文章!

推薦閱讀: