• async 与 await异步编程活用基础


    【本文转自:http://www.cnblogs.com/x-xk/archive/2013/06/05/3118005.html  作者:

    好久没写博客了,时隔5个月,奉上一篇精心准备的文章,希望大家能有所收获,对async 和 await 的理解有更深一层的理解。

    async 和 await 有你不知道的秘密,微软会告诉你吗?

    我用我自己的例子,去一步步诠释这个技术,看下去,你绝对会有收获。(渐进描述方式,愿适应所有层次的程序员)

    从零开始, 控制台 Hello World:

    什么?开玩笑吧?拿异步做Hello World??

    下面这个例子,输出什么?猜猜?

    复制代码
     1 static  void Main(string[] args) 
     2  {
     3 
     4      Task t = example1(); 
     5  } 
     6 
     7 static async  Task DoWork() 
     8 {
     9 
    10   Console.WriteLine("Hello World!"); 
    11   for (int i = 0; i < 3; i++) 
    12   { 
    13      Console.WriteLine("Working..{0}",i); 
    14      await  Task.Delay(1000);//以前我们用Thread.Sleep(1000),这是它的替代方式。 
    15   } 
    16 } 
    17 static async Task example1() 
    18 { 
    19     await DoWork(); 
    20     Console.WriteLine("First async Run End"); 
    21 }
    复制代码

    先不要看结果,来了解了解关键字吧,你确定你对async 和await了解?

    async 其实就是一个标记,标记这个方法是异步方法。

    当方法被标记为一个异步方法时,那么其方法中必须要使用await关键字。

    重点在await,看字面意思是等待,等待方法执行完成。

    它相当复杂,所以要细细讲述:

    当编译器看到await关键字后,其后的方法都会被转移到一个单独的方法中去独立运行。独立运行?

    是不是启动了另一个线程?

    嗯。有这个想法的同学,很不错。就是这个答案。

    我们来看看执行顺序。来验证一下我的这个说法,加深大家对await的理解。

    首先从入口example1 进入:

    ——>碰见await Dowork()

    ——>此时主线程返回

    ——>进入DoWork

    ——>输出“Hello World!”

    ——>碰见await 关键字

    ——>独立执行线程返回

    ——>运行结束 
    我们看到3个输出语句,按照我的说法,最终会出几个?猜猜,动手验证答案,往往是最实在的,大部分程序都不会骗我们。如果对Task有不熟悉的,可以参看本人博客先前写Task的部分

    1程序员就是喜欢折腾,明明一句话能搞定的 偏偏写这么多行。

    我们现在看到的就是,程序进入一个又一个方法后,输出个Hello World 就没了,没有结束,没有跳出。因为是异步,所以我们看不到后续的程序运行了。


    我为什么要用控制台来演示这个程序?

    这个疑问,让我做了下面的例子测试,一个深层次的问题,看不懂跳过没有丝毫影响。

    分析一下这个例子:

    复制代码
     1 static  void Main(string[] args) 
     2 { 
     3      example2(); 
     4 }
     5 
     6 static async void example2() 
     7 { 
     8     await DoWork(); 
     9     Console.WriteLine("First async Run End"); 
    10 }
    11 
    12 static async  Task DoWork() 
    13     { 
    14         Console.WriteLine("Hello World!"); 
    15         for (int i = 0; i < 3; i++) 
    16         { 
    17             await  Task.Delay(1000); 
    18             Console.WriteLine("Working..{0}",i); 
    19         } 
    20     } 
    复制代码

    运行丝毫问题,结果依旧是“Hello World ”,似乎更简单了。

    注意,细节来了,example2 是void,Mani也是void,这个相同点,似乎让我们可以这么做:

    复制代码
     1        static async void Main(string[] args)//给main加个 async 
     2        { 
     3            await DoWork(); 
     4        } 
     5      static async  Task DoWork() 
     6        { 
     7            Console.WriteLine("Hello World!"); 
     8            for (int i = 0; i < 3; i++) 
     9            { 
    10               await  Task.Delay(1000); 
    11                Console.WriteLine("Working..{0}",i); 
    12            } 
    13        } 
    复制代码


    程序写出,编译器没有错误,运行->

    2

      一个异步方法调用后将返回到它之前,它必须是完整的,并且线程依旧是活着的。

      而main正因为是控制台程序的入口,是主要的返回操作系统线程,所以编译器会提示入口点不能用async。

    下面这种事件,我想大家不会陌生吧?WPF似乎都用这种异步事件写法:

    1 private async void button1_Click(object sender, EventArgs e)
    2 {
    3 
    4   //….
    5 
    6 }

    以此列Main入口,类推在ASP.NET 的 Page_Load上也不要加async,因为异步Load事件内的其他异步都会一起执行,死锁? 还有比这更烦人的事吗?winfrom WPF的Load事件目前没有测试过,现在的事件都有异步async了,胡乱用,错了你都不知道找谁。

    好小细节提点到了,这个牵出的问题也就解决了。


    有心急的同学可能就纳闷了,第一个例子,怎么才能看到先前的输出啊?

    别急加上这句:

    1        static  void Main(string[] args) 
    2        { 
    3            Task t = example1();   
    4 
    5            t.Wait();//add 
    6        } 

    输出窗口就可以看到屏幕跳动连续输出了、、、

    入门示例已经介绍完了,来细细品味一下下面的知识吧。


     Async使用基础总结

    到此Async介绍了三种可能的返回类型:TaskTask<T>void

    但是async方法的固有返回类型只有Task和Task<T>,所以尽量避免使用async void。

    并不是说它没用,存在即有用,async void用于支持异步事件处理程序,什么意思?(比如我例子里面那些无聊的输出呀..)或者就如上述提到的:

    复制代码
    1 private async void button1_Click(object sender, EventArgs e)
    2 
    3 {
    4 
    5 //….
    6 
    7 }
    复制代码

    有兴趣的同学可以去找找(async void怎么支持异步事件处理程序)


     异常处理介绍

      async void 的方法具有不同的错误处理语义,因为在Task和Task<T>方法引发异常时,会捕获异常并将其置于Task对象上,方便我们查看错误信息,而async void,没有Task对象,没有对象直接导致异常都会直接在SynchronizationContext上引发(SynchronizationContext是async 和 await的实现底层哦)既然提到了SynchronizationContext,那么我在这说一句:

    SynchronizationContext 对任何编程人员来说都是有益的。

    无论是什么平台(ASP.NET、Windows 窗体、Windows Presentation Foundation (WPF)、Silverlight 或其他),所有 .NET 程序都包含 SynchronizationContext 概念。(建议好学的同学去找找)

    扯远了,回到之前谈到的 async void 和 Task 异常,看看两种异常的结果,看看测试用例。

    首先是 async void:

    复制代码
     1        static  void Main(string[] args) 
     2        { 
     3            AsyncVoidException(); 
     4        } 
     5        static async void ThrowExceptionAsync() 
     6        { 
     7            throw new OutOfMemoryException(); 
     8        } 
     9        static void AsyncVoidException() 
    10        { 
    11            try 
    12            { 
    13                ThrowExceptionAsync(); 
    14            } 
    15            catch (Exception) 
    16            {
    17 
    18                throw; 
    19            } 
    20        } 
    复制代码

    没有丝毫异常抛出,我先前说了,它会直接在SynchronizationContext抛出,但是执行异步的时候,它丝毫不管有没有异常,执行线程直接返回,异常直接被吞,所以根本无法捕获async void 的异常。我就不上图了,偷懒了。。

    再看看async Task测试用例:

    复制代码
     1        static  void Main(string[] args) 
     2        { 
     3            AsyncVoidException(); 
     4        } 
     5        static async Task ThrowExceptionAsync() 
     6        { 
     7            await Task.Delay(1000); 
     8            throw new OutOfMemoryException(); 
     9        } 
    10        static void AsyncVoidException() 
    11        { 
    12            try 
    13            { 
    14               Task t = ThrowExceptionAsync(); 
    15               t.Wait(); 
    16            } 
    17            catch (Exception) 
    18            {
    19 
    20                throw; 
    21            } 
    22        } 
    复制代码

    预料之中啊:

    4

    通过比较,大家不难看出哪个实用哪个不实用。

    对于async void 我还要闲扯一些缺点,让大家认识到,用这个的确要有扎实的根底。

      很显然async void 这个方法未提供一种简单的方式,去通知向调用它的代码发出回馈信息,通知是否已经执行完成。

      启动async void方法不难,但你要确定它何时结束也是不易。

      async void 方法会在启动和结束时去通知SynchronizationContext。简单的说,要测试async void 不是件简单的事,但有心去了解,SynchronizationContext或许就不这么难了,它完全可以用来检测async void 的异常。

         说了这么多缺点,该突出些重点了:

        建议多使用async Task 而不是async void。

        async Task方法便于实现错误处理、可组合性和可测试性。

        不过对于异步事件处理程序不行,这类处理程序必须返回void。

    异步——我们既陌生又熟悉的朋友——死锁!

      对于异步编程不了解的程序员,或许常干这种事:

      混合使用同步和异步代码,他们仅仅转换一小部分应用程序,提出一段代码块,然后用同步API包装它,这么做方便隔离,同步分为一块,异步分为另一块,这么做的后果是,他们常常会遇到和死锁有关的问题。

    我之前一直用控制台来写异步,大家应该觉得,异步Task就是这么用的吧?没有丝毫阻噻,都是理所当然的按计划运行和结束。

      嗯,来个简单的例子,看看吧:

    这是我的WPF项目的测试例子:

    复制代码
     1  int i = 0; 
     2  private void button_1_Click(object sender, RoutedEventArgs e) 
     3  { 
     4     
     5     textBox.Text += "你点击了按钮 "+i++.ToString()+"	
    "; 
     6     Task t = DelayAsync(); 
     7     t.Wait(); 
     8  }  
     9  private static async Task DelayAsync() 
    10  {
    11 
    12     MessageBox.Show("异步完成"); 
    13     await Task.Delay(1000); 
    14  } 
    复制代码

    为了便于比较,看看控制台对应的代码:

    复制代码
     1        static  void Main(string[] args) 
     2        { 
     3            Task t = DelayAsync(); 
     4            t.Wait(); 
     5        } 
     6        private static async Task DelayAsync() 
     7        { 
     8            await Task.Delay(1000); 
     9            Console.WriteLine("Complet"); 
    10        }
    复制代码

    控制台程序没有丝毫问题,我保证。

    现在来注意一下WPF代码,当我button点击之后,应该出现的效果是:

    5

      看图片的效果不错。

      接着你关掉提示框,你会发现 ,这个窗口点什么都没用了。关闭的不行,我确定我说的没错。

      想关掉 就去任务管理器里面结束进程吧~~~

      这是一个很简单的死锁示例,我想说的是差不多的代码,在不同的应用程序里面会有不一样的效果,这就是它灵活和复杂的地方

      这种死锁的根本原因是await处理上下文的方式。

      默认情况下,等待未完成的Task时,会捕获当前“上下文”,在Task完成时使用该上下文回复方法的执行(这里的“上下文”指的是当前TaskScheduler任务调度器)

      值得注意的就是下面这几句代码:

    复制代码
    1   t.Wait();
    2 
    3 private static async Task DelayAsync() 
    4 {
    5 
    6   MessageBox.Show("异步完成"); 
    7   await Task.Delay(1000); 
    8 }
    复制代码

     请确定你记住他的结构了,现在我来细讲原理。

      Task t  有一个线程块在等待着 DelayAsync 的执行完成。

      而 async Task DelayAsunc 在另一个线程块中执行。

      也就是说,在 MessageBox.Show("异步完成");   这个方法完成后,await 会继续获取 async 余下的部分,它还能捕获到接下来的代码吗?

    async的线程已经被t线程在等待了,t在等待 async的完成,而运行Task.Delay(1000)后,await就会尝试在捕获的上下文中执行async方法的剩余部分,async被占用了,它就在等待t。然后它们就相互等待对方,从而导致死锁,锁上就不听使唤了~~~用个图来形容一下这个场景

    777

    说重点了。

      为什么控制带应用程序不会形成这种死锁?

      它们具有线程池SynchronizationContext(同步上下文),而不是每次执行一个线程块区的SynchronizationContext,以此当await完成时,它会在线程池上安排async方法的剩余部分。所以各位,在控制台写好的异步程序,移动到别的应用程序中就可能会发生死锁。


      好,现在来解决这个WPF的异步错误,我想这应该会引起大家兴趣,解决问题是程序员最喜欢的活。

    改Wait()为ConfigureAwait(false)像这样:

    1 Task t = DelayAsync();
    2 
    3 t.ConfigureAwait(continueOnCapturedContext:false);//这个写法复杂了点,但从可读性角度来说是不错的,你这么写t.ConfigureAwait(false)当然也没问题

    什么是ConfigureAwait?

    官方解释:试图继续回夺取的原始上下文,则为 true,否则为 false。

    不好理解,我来详细解释下,这个方法是很有用的,它可以实现少量并行性

      使得某些异步代码可以并行运行,而不是一个个去执行,进行零碎的线程块工作,提高性能。

    另一方面才是重点,它可以避免死锁。

      Wait造成的相互等待,在用这个方法的时候,就能顺利完成,如意料之中自然。当然还有指导意见要说的,如果在方法中的某处使用ConfigureAwait,则建议对该方法中,此后每个await都使用它。

         说到这,只怕有些同学觉得,能避免死锁,这么好!以后就用ConfigureAwait就行了,不用什么await了。

    没有一种指导方式是让程序员盲目使用的,ConfigureAwait这个方法,在需要上下文的代码中是用不了的。看不懂?没关系,接着看。

      await运行的是一种原始上下文,就比如这样:

    1  static async Task example1() 
    2  { 
    3      await DoWork(); 
    4      Console.WriteLine("First async Run End"); 
    5  }

      一个async对应一个await ,它们本身是一个整体,我们称它为原始上下文。

    ConfigureAwait而它有可能就不是原始上下文,因为它的作用是试图夺回原始上下文。用的时候VS2012会帮我们自动标识出来:

    8

    出这个问题是我在事件前加了一个async声明。

      添加异步标识后,ConfigureAwait就不能夺取原始上下文了,在这种情况下,事件处理程序是不能放弃原始上下文。

    大家要知道的是:

      每个async方法都有自己的上下文,如果一个async方法去调用另一个async方法,则其上下文是相互独立的。

    为什么这么说?独立是什么意思?我拿个例子说明吧:

    复制代码
     1  private async void button_1_Click(object sender, RoutedEventArgs e) 
     2  { 
     3     Task t = DelayAsunc(); 
     4     
     5      t.ConfigureAwait(false);//Error 
     6 
     7  }  
     8  private static async Task DelayAsunc() 
     9  { 
    10     MessageBox.Show("异步完成"); 
    11     await Task.Delay(1000); 
    12  } 
    复制代码

     
    因为是独立的,所以ConfigureAwait不能夺取原始上下文,错误就如上那个图。

    修改一下:

    复制代码
     1  private async void button_1_Click(object sender, RoutedEventArgs e) 
     2  { 
     3    Task t = DelayAsunc(); 
     4 
     5    t.Wait(); 
     6  } 
     7  private static async Task DelayAsunc() 
     8  { 
     9     MessageBox.Show("异步完成"); 
    10     await Task.Delay(1000).ConfigureAwait(continueOnCapturedContext:false); 
    11  }
    复制代码

    每个async 都有自己的上下文,因为独立所以它们之间的调用是可行的。

    修改后的例子,将事件处理程序的所有核心逻辑都放在一个可测试且无上下文的async Task方法中,仅在上下文相关事件处理程序中保存最少量的代码。

    至此,已经总结了3条异步编程指导原则,我一起集合一下这3条,方便查阅。

    9

       

      我们都忽略了一个问题,可能大家从来都没想过,

    我们对代码操作,一直都是一种异步编程。而我们的代码都运行在一个操作系统线程!

    来看些最简单的应用,帮助大家能快速的熟悉,并使用,才是我想要达到的目的,你可以不熟练,可以不会用,但是,你可以去主动接近它,适应它,熟悉它,直到完全活用。

    异步编程是重要和有用的。

    下面来做些基本功的普及。我先前提到UI线程,什么是UI线程?

    我们都碰见过程序假死状态,冻结,无响应。

    微软提供了UI框架,使得你可以使用C#操作所有UI线程,虽说是UI框架,我想大家都听过,它们包括:WinForms,WPF,Silverlight。

    UI线程是唯一的一个可以控制一个特定窗口的线程,也是唯一的线程能检测用户的操作,并对它们做出响应。

      这次介绍就到这了。

  • 相关阅读:
    php (一)
    php 运算符
    Python 元组
    Python 深拷贝和浅拷贝的区别
    Python 列表
    Python 字符串
    Python 循环控制
    Python 随机数,数学
    bzoj5018 [Snoi2017]英雄联盟
    bzoj5015 [Snoi2017]礼物
  • 原文地址:https://www.cnblogs.com/cncc/p/6248748.html
Copyright © 2020-2023  润新知