C# 从做早餐看同步异步

概述

一天之计在于晨,每天的早餐也是必不可少,但是很多人为了节约时间,都是简单的吃点凑合一下或干脆不吃早餐,这对于个人身体和工作效率来说,无疑是不合理的,那么要如何做一顿早餐呢?如何能节约做早餐的时间呢?本文以一个简单的小例子,简述如何做一顿早餐及如何优化做早餐的时间。仅供学习分享使用,如有不足之处,还请指正。

正常情况下,做早餐可以分为以下几个步骤:

倒一杯咖啡。

加热平底锅,然后煎两个鸡蛋。

煎三片培根。

烤两片面包。

在烤面包上加黄油和果酱。

倒一杯橙汁。

同步方式做早餐

根据以上步骤进行编程,做一份早餐需要编写程序如下:

  1        /// <summary>

  2        /// 同步做早餐

  3        /// </summary>

  4        /// <param name="sender"></param>

  5        /// <param name="e"></param>

  6        private void btnBreakfast_Click(object sender, EventArgs e)

  7        {

  8            this.txtInfo.Clear();

  9            Stopwatch watch = Stopwatch.StartNew();

10            watch.Start();

11            //1. 倒一杯咖啡。

12            string cup = PourCoffee();

13            PrintInfo("咖啡冲好了");

14            //2. 加热平底锅,然后煎两个鸡蛋。

15            string eggs = FryEggs(2);

16            PrintInfo("鸡蛋煎好了");

17            //3. 煎三片培根。

18            string bacon = FryBacon(3);

19            PrintInfo("培根煎好了");

20            //4. 烤两片面包。

21            string toast = ToastBread(2);

22            //5. 在烤面包上加黄油和果酱。

23            ApplyButter(toast);

24            ApplyJam(toast);

25            PrintInfo("面包烤好了");

26            //6. 倒一杯橙汁。

27            string oj = PourOJ();

28            PrintInfo("橙汁倒好了");

29            PrintInfo("早餐准备完毕!");

30            watch.Stop();

31            TimeSpan time = watch.Elapsed;

32            PrintInfo(string.Format("总运行时间为:{0}秒", time.TotalSeconds.ToString("0.00")));

33        }

34

35        /// <summary>

36        /// 倒一杯咖啡

37        /// </summary>

38        /// <returns></returns>

39        private string PourCoffee()

40        {

41            PrintInfo("正在冲咖啡...");

42            return "咖啡";

43        }

44

45        /// <summary>

46        /// 抹果酱

47        /// </summary>

48        /// <param name="toast"></param>

49        private void ApplyJam(string toast) =>

50            PrintInfo("往面包抹果酱");

51

52        /// <summary>

53        /// 抹黄油

54        /// </summary>

55        /// <param name="toast"></param>

56        private void ApplyButter(string toast) =>

57            PrintInfo("往面包抹黄油");

58

59        /// <summary>

60        /// 烤面包

61        /// </summary>

62        /// <param name="slices"></param>

63        /// <returns></returns>

64        private string ToastBread(int slices)

65        {

66            for (int slice = 0; slice < slices; slice++)

67            {

68                PrintInfo("往烤箱里面放面包");

69            }

70            PrintInfo("开始烤...");

71            Task.Delay(3000).Wait();

72            PrintInfo("从烤箱取出面包");

73

74            return "烤面包";

75        }

76

77        /// <summary>

78        /// 煎培根

79        /// </summary>

80        /// <param name="slices"></param>

81        /// <returns></returns>

82        private string FryBacon(int slices)

83        {

84            PrintInfo($"放 {slices} 片培根在平底锅");

85            PrintInfo("煎第一片培根...");

86            Task.Delay(3000).Wait();

87            for (int slice = 0; slice < slices; slice++)

88            {

89                PrintInfo("翻转培根");

90            }

91            PrintInfo("煎第二片培根...");

92            Task.Delay(3000).Wait();

93            PrintInfo("把培根放盘子里");

94

95            return "煎培根";

96        }

97

98        /// <summary>

99        /// 煎鸡蛋

100        /// </summary>

101        /// <param name="howMany"></param>

102        /// <returns></returns>

103        private string FryEggs(int howMany)

104        {

105            PrintInfo("加热平底锅...");

106            Task.Delay(3000).Wait();

107            PrintInfo($"磕开 {howMany} 个鸡蛋");

108            PrintInfo("煎鸡蛋 ...");

109            Task.Delay(3000).Wait();

110            PrintInfo("鸡蛋放盘子里");

111

112            return "煎鸡蛋";

113        }

114

115        /// <summary>

116        /// 倒橙汁

117        /// </summary>

118        /// <returns></returns>

119        private string PourOJ()

120        {

121            PrintInfo("倒一杯橙汁");

122            return "橙汁";

123        }

同步做早餐示例

通过运行示例,发现采用同步方式进行编程,做一份早餐,共计15秒钟,且在此15秒钟时间内,程序处于【卡住】状态,无法进行其他操作。如下所示:

同步做早餐示意图

同步方式做早餐,就是一个做完,再进行下一个,顺序执行,如下所示:


同步方式为何会【卡住】?

因为在程序进程中,会有一个主线程,用于响应用户的操作,同步方式下,做早餐的和前端页面同在主线程中,所以当开始做早餐时,就不能响应其他的操作了。这就是【两耳不闻窗外事,一心只读圣贤书】的境界。但如果让用户长时间处于等待状态,会让用户体验很不友好。比如,刘玄德三顾茅庐,大雪纷飞之下,诸葛亮在草庐中午睡,刘关张在大雪中静等。试问有几人会有玄德的耐心,何况程序也不是诸葛亮,用户也没有玄德的耐心!

异步方式做早餐

上述代码演示了不正确的实践:构造同步代码来执行异步操作。 顾名思义,此代码将阻止执行这段代码的线程执行任何其他操作。 在任何任务进行过程中,此代码也不会被中断。 就如同你将面包放进烤面包机后盯着此烤面包机一样。 你会无视任何跟你说话的人,直到面包弹出。如何做才能避免线程阻塞呢?答案就是异步。 await 关键字提供了一种非阻塞方式来启动任务,然后在此任务完成时继续执行。

首先更新代码,对于耗时的程序,采用异步方式做早餐,如下所示:

1        private async void btnBreakfastAsync_Click(object sender, EventArgs e)

2        {

3            this.txtInfo.Clear();

4            Stopwatch watch = Stopwatch.StartNew();

5            watch.Start();

6            //1. 倒一杯咖啡。

7            string cup = PourCoffee();

8            PrintInfo("咖啡冲好了");

9            //2. 加热平底锅,然后煎两个鸡蛋。

10            //Task<string> eggs = FryEggsAsync(2);

11            string eggs =await FryEggsAsync(2);

12            PrintInfo("鸡蛋煎好了");

13            //3. 煎三片培根。

14            string bacon =await FryBaconAsync(3);

15            PrintInfo("培根煎好了");

16            //4. 烤两片面包。

17            string toast =await ToastBreadAsync(2);

18            //5. 在烤面包上加黄油和果酱。

19            ApplyButter(toast);

20            ApplyJam(toast);

21            PrintInfo("面包烤好了");

22            //6. 倒一杯橙汁。

23            string oj = PourOJ();

24            PrintInfo("橙汁倒好了");

25            PrintInfo("早餐准备完毕!");

26            watch.Stop();

27            TimeSpan time = watch.Elapsed;

28            PrintInfo(string.Format("总运行时间为:{0}秒", time.TotalSeconds.ToString("0.00")));

29        }

30

31        /// <summary>

32        /// 异步烤面包

33        /// </summary>

34        /// <param name="slices"></param>

35        /// <returns></returns>

36        private async Task<string> ToastBreadAsync(int slices)

37        {

38            for (int slice = 0; slice < slices; slice++)

39            {

40                PrintInfo("往烤箱里面放面包");

41            }

42            PrintInfo("开始烤...");

43            await Task.Delay(3000);

44            PrintInfo("从烤箱取出面包");

45

46            return "烤面包";

47        }

48

49        /// <summary>

50        /// 异步煎培根

51        /// </summary>

52        /// <param name="slices"></param>

53        /// <returns></returns>

54        private async Task<string> FryBaconAsync(int slices)

55        {

56            PrintInfo($"放 {slices} 片培根在平底锅");

57            PrintInfo("煎第一片培根...");

58            await Task.Delay(3000);

59            for (int slice = 0; slice < slices; slice++)

60            {

61                PrintInfo("翻转培根");

62            }

63            PrintInfo("煎第二片培根...");

64            await Task.Delay(3000);

65            PrintInfo("把培根放盘子里");

66

67            return "煎培根";

68        }

69

70        /// <summary>

71        /// 异步煎鸡蛋

72        /// </summary>

73        /// <param name="howMany"></param>

74        /// <returns></returns>

75        private async Task<string> FryEggsAsync(int howMany)

76        {

77            PrintInfo("加热平底锅...");

78            await Task.Delay(3000);

79            PrintInfo($"磕开 {howMany} 个鸡蛋");

80            PrintInfo("煎鸡蛋 ...");

81            await Task.Delay(3000);

82            PrintInfo("鸡蛋放盘子里");

83

84            return "煎鸡蛋";

85        }

注意:通过测试发现,异步方式和同步方式的执行时间一致,所以采用异步方式并不会缩短时间,但是程序已不再阻塞,可以同时响应用户的其他请求。

优化异步做早餐

通过上述异步方式,虽然优化了程序,不再阻塞,但是时间并没有缩短,那么要如何优化程序来缩短时间,以便早早的吃上可口的早餐呢?答案就是在开始一个任务后,在等待任务完成时,可以继续进行准备其他的任务。 你也几乎将在同一时间完成所有工作。 你将吃到一顿热气腾腾的早餐。通过合并任务和调整任务的顺序,将大大节约任务的完成时间,如下所示:

1        /// <summary>

2        /// 优化异步做早餐

3        /// </summary>

4        /// <param name="sender"></param>

5        /// <param name="e"></param>

6        private async void btnBreakfast2_Click(object sender, EventArgs e)

7        {

8            this.txtInfo.Clear();

9            Stopwatch watch = Stopwatch.StartNew();

10            watch.Start();

11            //1. 倒一杯咖啡。

12            string cup = PourCoffee();

13            PrintInfo("咖啡冲好了");

14            //2. 加热平底锅,然后煎两个鸡蛋。

15            Task<string> eggsTask = FryEggsAsync(2);

16            //3. 煎三片培根。

17            Task<string> baconTask = FryBaconAsync(3);

18            //4.5合起来 烤面包,抹果酱,黄油

19            Task<string> toastTask = MakeToastWithButterAndJamAsync(2);

20

21            string eggs = await eggsTask;

22            PrintInfo("鸡蛋煎好了");

23

24            string bacon = await baconTask;

25            PrintInfo("培根煎好了");

26

27            string toast = await toastTask;

28            PrintInfo("面包烤好了");

29            //6. 倒一杯橙汁。

30            string oj = PourOJ();

31            PrintInfo("橙汁倒好了");

32            PrintInfo("早餐准备完毕!");

33            watch.Stop();

34            TimeSpan time = watch.Elapsed;

35            PrintInfo(string.Format("总运行时间为:{0}秒", time.TotalSeconds.ToString("0.00")));

36        }

37

38        /// <summary>

39        /// 组合任务

40        /// </summary>

41        /// <param name="number"></param>

42        /// <returns></returns>

43        private async Task<string> MakeToastWithButterAndJamAsync(int number)

44        {

45            var toast = await ToastBreadAsync(number);

46            ApplyButter(toast);

47            ApplyJam(toast);

48            return toast;

49        }

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

通过以上优化示例发现,通过合并任务和调整顺序,做一份早餐,需要6.06秒。

优化异步早餐示意图

优化后的异步做早餐,由于一些任务并发运行,因此节约了时间。示意图如下所示:

异步异常

上述示例假定所有的任务都可以正常完成,那么如果某一个任务执行过程中发生了异常,要如何捕获呢?答案是:当任务无法成功完成时,它们将引发异常。 当启动的任务为 awaited 时,客户端代码可捕获这些异常。

例如当烤面包的时候,烤箱突然着火了,如何处理异常呢?代码如下所示:

1        private async void btnBreakfastAsync3_Click(object sender, EventArgs e)

2        {

3            try

4            {

5                this.txtInfo.Clear();

6                Stopwatch watch = Stopwatch.StartNew();

7                watch.Start();

8                //1. 倒一杯咖啡。

9                string cup = PourCoffee();

10                PrintInfo("咖啡冲好了");

11                //2. 加热平底锅,然后煎两个鸡蛋。

12                Task<string> eggsTask = FryEggsAsync(2);

13                //3. 煎三片培根。

14                Task<string> baconTask = FryBaconAsync(3);

15                //4.5合起来 烤面包,抹果酱,黄油

16                Task<string> toastTask = MakeToastWithButterAndJamAsyncEx(2);

17

18                string eggs = await eggsTask;

19                PrintInfo("鸡蛋煎好了");

20

21                string bacon = await baconTask;

22                PrintInfo("培根煎好了");

23

24                string toast = await toastTask;

25                PrintInfo("面包烤好了");

26                //6. 倒一杯橙汁。

27                string oj = PourOJ();

28                PrintInfo("橙汁倒好了");

29                PrintInfo("早餐准备完毕!");

30                watch.Stop();

31                TimeSpan time = watch.Elapsed;

32                PrintInfo(string.Format("总运行时间为:{0}秒", time.TotalSeconds.ToString("0.00")));

33            }

34            catch (AggregateException ex) {

35                PrintInfo("线程内部异常");

36                PrintInfo(ex.StackTrace);

37            }

38            catch (Exception ex)

39            {

40                PrintInfo("其他异常");

41                PrintInfo(ex.Message);

42            }

43        }

44

45        /// <summary>

46        /// 组合任务

47        /// </summary>

48        /// <param name="number"></param>

49        /// <returns></returns>

50        private async Task<string> MakeToastWithButterAndJamAsyncEx(int number)

51        {

52            var toast = await ToastBreadAsyncEx(number);

53            ApplyButter(toast);

54            ApplyJam(toast);

55            return toast;

56        }

57

58        /// <summary>

59        /// 异步烤面包异常

60        /// </summary>

61        /// <param name="slices"></param>

62        /// <returns></returns>

63        private async Task<string> ToastBreadAsyncEx(int slices)

64        {

65            for (int slice = 0; slice < slices; slice++)

66            {

67                PrintInfo("往烤箱里面放面包");

68            }

69            PrintInfo("开始烤...");

70            await Task.Delay(2000);

71            PrintInfo("着火了! 面包糊了!");

72            int a = 1, b = 0;

73            int i = a / b;//制造一个异常

74            //throw new InvalidOperationException("烤箱着火了!");

75            await Task.Delay(1000);

76            PrintInfo("从烤箱取出面包");

77

78            return "烤面包";

79        }

异步任务异常示例

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

1        private async void btnBreakfastAsync4_Click(object sender, EventArgs e)

2        {

3            this.txtInfo.Clear();

4            Stopwatch watch = Stopwatch.StartNew();

5            watch.Start();

6            //1. 倒一杯咖啡。

7            string cup = PourCoffee();

8            PrintInfo("咖啡冲好了");

9            //2. 加热平底锅,然后煎两个鸡蛋。

10            Task<string> eggsTask = FryEggsAsync(2);

11            //3. 煎三片培根。

12            Task<string> baconTask = FryBaconAsync(3);

13            //4.5合起来 烤面包,抹果酱,黄油

14            Task<string> toastTask = MakeToastWithButterAndJamAsync(2);

15            //等待任务完成

16            await Task.WhenAll(eggsTask, baconTask, toastTask);

17           

18            PrintInfo("鸡蛋煎好了");

19            PrintInfo("培根煎好了");

20            PrintInfo("面包烤好了");

21            //6. 倒一杯橙汁。

22            string oj = PourOJ();

23            PrintInfo("橙汁倒好了");

24            PrintInfo("早餐准备完毕!");

25            watch.Stop();

26            TimeSpan time = watch.Elapsed;

27            PrintInfo(string.Format("总运行时间为:{0}秒", time.TotalSeconds.ToString("0.00")));

28        }

另一种选择是使用 WhenAny,它将返回一个当其参数完成时才完成的 Task<Task>。如下所示:

1        private async void btnBreakfastAsync5_Click(object sender, EventArgs e)

2        {

3            this.txtInfo.Clear();

4            Stopwatch watch = Stopwatch.StartNew();

5            watch.Start();

6            //1. 倒一杯咖啡。

7            string cup = PourCoffee();

8            PrintInfo("咖啡冲好了");

9            //2. 加热平底锅,然后煎两个鸡蛋。

10            Task<string> eggsTask = FryEggsAsync(2);

11            //3. 煎三片培根。

12            Task<string> baconTask = FryBaconAsync(3);

13            //4.5合起来 烤面包,抹果酱,黄油

14            Task<string> toastTask = MakeToastWithButterAndJamAsync(2);

15            //等待任务完成

16            var breakfastTasks = new List<Task> { eggsTask, baconTask, toastTask };

17            while (breakfastTasks.Count > 0)

18            {

19                Task finishedTask = await Task.WhenAny(breakfastTasks);

20                if (finishedTask == eggsTask)

21                {

22                    PrintInfo("鸡蛋煎好了");

23                }

24                else if (finishedTask == baconTask)

25                {

26                    PrintInfo("培根煎好了");

27                }

28                else if (finishedTask == toastTask)

29                {

30                    PrintInfo("面包烤好了");

31                }

32                breakfastTasks.Remove(finishedTask);

33            }

34            //6. 倒一杯橙汁。

35            string oj = PourOJ();

36            PrintInfo("橙汁倒好了");

37            PrintInfo("早餐准备完毕!");

38            watch.Stop();

39            TimeSpan time = watch.Elapsed;

40            PrintInfo(string.Format("总运行时间为:{0}秒", time.TotalSeconds.ToString("0.00")));

41        }

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

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 216,287评论 6 498
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,346评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 162,277评论 0 353
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,132评论 1 292
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,147评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,106评论 1 295
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,019评论 3 417
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,862评论 0 274
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,301评论 1 310
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,521评论 2 332
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,682评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,405评论 5 343
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,996评论 3 325
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,651评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,803评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,674评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,563评论 2 352

推荐阅读更多精彩内容