【本文转自:http://www.cnblogs.com/x-xk/archive/2013/06/05/3118005.html 做者:肅】html
很久没写博客了,时隔5个月,奉上一篇精心准备的文章,但愿你们能有所收获,对async 和 await 的理解有更深一层的理解。程序员
async 和 await 有你不知道的秘密,微软会告诉你吗?编程
我用我本身的例子,去一步步诠释这个技术,看下去,你绝对会有收获。(渐进描述方式,愿适应全部层次的程序员)框架
从零开始, 控制台 Hello World:异步
什么?开玩笑吧?拿异步作Hello World??async
下面这个例子,输出什么?猜猜?异步编程
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关键字。spa
重点在await,看字面意思是等待,等待方法执行完成。
它至关复杂,因此要细细讲述:
当编译器看到await关键字后,其后的方法都会被转移到一个单独的方法中去独立运行。独立运行?
是否是启动了另外一个线程?
嗯。有这个想法的同窗,很不错。就是这个答案。
咱们来看看执行顺序。来验证一下个人这个说法,加深你们对await的理解。
首先从入口example1 进入:
——>遇见await Dowork()
——>此时主线程返回
——>进入DoWork
——>输出“Hello World!”
——>遇见await 关键字
——>独立执行线程返回
——>运行结束
咱们看到3个输出语句,按照个人说法,最终会出几个?猜猜,动手验证答案,每每是最实在的,大部分程序都不会骗咱们。若是对Task有不熟悉的,能够参看本人博客先前写Task的部分
咱们如今看到的就是,程序进入一个又一个方法后,输出个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 }
程序写出,编译器没有错误,运行->
一个异步方法调用后将返回到它以前,它必须是完整的,而且线程依旧是活着的。
而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介绍了三种可能的返回类型:Task,Task<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,那么我在这说一句:
不管是什么平台(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 }
预料之中啊:
经过比较,你们不难看出哪一个实用哪一个不实用。
对于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()+"\t\n"; 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点击以后,应该出现的效果是:
看图片的效果不错。
接着你关掉提示框,你会发现 ,这个窗口点什么都没用了。关闭的不行,我肯定我说的没错。
想关掉 就去任务管理器里面结束进程吧~~~
这是一个很简单的死锁示例,我想说的是差很少的代码,在不一样的应用程序里面会有不同的效果,这就是它灵活和复杂的地方。
这种死锁的根本缘由是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。而后它们就相互等待对方,从而致使死锁,锁上就不听使唤了~~~用个图来形容一下这个场景
说重点了。
为何控制带应用程序不会造成这种死锁?
它们具备线程池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会帮咱们自动标识出来:
出这个问题是我在事件前加了一个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条,方便查阅。
咱们都忽略了一个问题,可能你们历来都没想过,
咱们对代码操做,一直都是一种异步编程。而咱们的代码都运行在一个操做系统线程!
来看些最简单的应用,帮助你们能快速的熟悉,并使用,才是我想要达到的目的,你能够不熟练,能够不会用,可是,你能够去主动接近它,适应它,熟悉它,直到彻底活用。
异步编程是重要和有用的。
下面来作些基本功的普及。我先前提到UI线程,什么是UI线程?
咱们都遇见过程序假死状态,冻结,无响应。
微软提供了UI框架,使得你可使用C#操做全部UI线程,虽然说是UI框架,我想你们都听过,它们包括:WinForms,WPF,Silverlight。
UI线程是惟一的一个能够控制一个特定窗口的线程,也是惟一的线程能检测用户的操做,并对它们作出响应。
此次介绍就到这了。