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;        }

在本例中,合并了【烤面包+抹果酱+抹黄油】为一个任务,这样是烤面包的同时,可以煎鸡蛋,煎培根,三项耗时任务同时执行。在三个任务都完成是,早餐也就做好了,示例如下所示:

图片

请注意,从烤面包机着火到发现异常,有相当多的任务要完成。当异步运行的任务引发异常时,该任务出错。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")));        }

以上就是由同步到异步再到优化异步任务的逐步过程,旨在抛砖引玉,一起学习,共同进步。