usercount

异步编程系列第05章 Await究竟做了什么?

写在前面

  在学异步,有位园友推荐了《async in C#5.0》,没找到中文版,恰巧也想提高下英文,用我拙劣的英文翻译一些重要的部分,纯属娱乐,简单分享,保持学习,谨记谦虚。

  如果你觉得这件事儿没意义翻译的又差,尽情的踩吧。如果你觉得值得鼓励,感谢留下你的赞,愿爱技术的园友们在今后每一次应该猛烈突破的时候,不选择知难而退。在每一次应该独立思考的时候,不选择随波逐流,应该全力以赴的时候,不选择尽力而为,不辜负每一秒存在的意义。

   转载和爬虫请注明原文链接http://www.cnblogs.com/tdws/p/5659003.html,博客园 蜗牛 2016年6月27日。

目录

第01章 异步编程介绍

第02章 为什么使用异步编程

第03章 手动编写异步代码

第04章 编写Async方法

第05章 Await究竟做了什么

第06章 以Task为基础的异步模式

第07章 异步代码的一些工具

第08章 哪个线程在运行你的代码

第09章 异步编程中的异常

第10章 并行使用异步编程

第11章 单元测试你的异步代码

第12章 ASP.NET应用中的异步编程

第13章 WinRT应用中的异步编程

第14章 编译器在底层为你的异步做了什么

第15章 异步代码的性能

await究竟做了什么?

  我们有两种角度来看待C#5.0的async功能特性,尤其是await关键字上发生了什么:

  ·作为一个语言的功能特性,他是一个供你学习的已经定义好的行为

  ·作为一个在编译时的转换,这是一个C#语法糖,为了简略之前复杂的异步代码

  这都是真的;它们就像同一枚硬币的两面。在本章,我们将会集中在第一点上来探讨异步。在第十四章我们将会从另一个角度来探讨,即更复杂的,但是提供了一些细节使debug和性能考虑更加清晰。

休眠和唤醒一个方法

   当你的程序执行遇到await关键字时,我们想要发生两件事:

   ·为了使你的代码异步,当前执行你代码的线程应该被释放。这意味着,在普通,同步的角度来看,你的方法应该返回。

   ·当你await的Task完成时,你的方法应该从之前的位置继续,就像它没在早些时候被返回。

  为了做到这个行为,你的方法必须在遇到await时暂停,然后在将来的某个时刻恢复执行。

  我把这个过程当做一个休眠一台计算机的小规模情况来看(S4 sleep)。这个方法当前的状态会被存储起来(译者:状态存储起来,正如我们第二章厨房那个例子,厨师会把已放在烤箱中的食物的烹饪状态以标签的形式贴在上面),并且这个方法完全退出(厨师走了,可能去做其他事情了)。当一台计算机休眠,计算机的动态数据和运行数据被保存到磁盘,并且变得完全关闭。下面这段话和计算机休眠大概一个道理,一个正在await的方法除了用一点内存,不使用其他资源,那么可以看作这个正执行的线程已经被释放。

       进一步采取类似上一段的类比:一个阻塞型方法更像你暂停一台计算机(S3 sleep),它虽然使用较少的资源,但从根本上来讲它一直在运行着。

  在理想的情况下,我们希望编程者察觉不到这里的休眠。尽管实际上休眠和唤醒一个方法的中期执行是很复杂的,C#也将会确保你的代码被唤醒,就像什么都没发生一样。(译者:不得不赞叹微软对语法糖的封装和处理)。

方法的状态

  为了准确的弄清楚在你使用await时C#到底为我们做了多少事情,我想列出所有关于方法状态的所有我们记住和了解的细节。

  首先,你方法中本地的变量的值会被记住,包括以下值:

  ·你方法的参数

  ·在本范围内所有你定义的变量

  ·其他变量包括循环数

  ·如果你的方法非静态,那么包括this变量。这样,你类的成员变量在方法唤醒时都是可用的。

  他们都被存在.NET 垃圾回收堆(GC堆)的一个对象上。因此当你使用await时,一个消耗一些资源的对象将会被分配,但是在大多数情况下不用担心性能问题。

  C#也会记住在方法的什么位置会执行到await。这可以使用数字存储起来,用来表示await关键字在当前方法的位置。

  在关于如何使用await关键字没有什么特别的限制,例如,他们可以被用在一个长表达式上,可能包含不止一个await:

int myNum = await AlexsMethodAsync(await myTask, await StuffAsync());

  为了去记住剩余部分的表达式的状态在await某些东西时,增加了额外的条件。比如,当我们运行await StuffAsync()时,await myTask的结果需要被记住。.NET中间语言(IL)在栈上存储这种子类表达式,因此 ,这个栈就是我们await关键字需要存储的。

  最重要的是,当程序执行到第一个await关键字时,方法便返回了(译者:关于方法在遇到await时返回,建议读者从第一章拆分的两个方法来理解)。如果它不是一个async void方法,一个Task在这个时刻被返回,因此调用者可以等待我们以某种方式完成。C#也必须存储一种操作返回的Task的方式,这样当你的方法完成,这个Task也变得completed,并且执行者也可以返回到方法的异步链当中。确切的机制将会在第十四章中介绍。

上下文

  作为一个使await的过程尽量透明的部分,C#捕捉各种上下文在遇到await时,然后在恢复方法使将其恢复。

  在所有事情中最重要的还是同步上下文(synchronization context),即可以被用于恢复方法在一个特殊类型的线程上。这对于UI app尤其重要,就是那种只能在正确的线程上操作UI的(就是winform wpf之类的)。同步上下文是一个复杂的话题,第八章将会详细解释。

  其他类型的上下文也会被从当前调用的线程捕捉。他们的控制是通过一个相同名称的类来实现的,所以我将列出一些重要的上下文类型:

  ExecutionContext

  这是父级上下文,所有其他上下文都是它的一部分。这是.NET的系统功能,如Task使用其捕捉和传播上下文,但是它本身不包含什么行为。

  SecurityContext

  这是我们发现并找到通常被限制在当前线程的安全信息的地方。如果你的代码需要运行在特定的用户,你也许会,模拟或者扮演这个用户,或者ASP.NET将会帮你实现扮演。在这种情况下,模拟信息会存在SecurityContext。

  CallContext(这个东西耳熟能详吧,相信用过EF的都知道)

  这允许编程者存储他们在逻辑线程的生命周期中一直可用的数据。即使考虑到在很多情况下有不好的表现,它仍然可以避免程序中方法的参数传来传去。(译者:因为你存到callcontext里,随时都可以获取呀,不用通过传参数传来传去了)。LogicalCallContextis是一个相关的可以跨用应用程序域的。

       值得注意的是线程本地存储(TLS),它和CallContext的目标相似,但它在异步的情况下是不工作的,因为在一个耗时操作中,线程被释放掉了,并且可能被用于处理其他事情了。你的方法也许被唤醒并执行在一个不同的线程上。

  C#将会在你方法恢复(resume,这里就是单纯的“恢复”)的时候恢复(restore,我觉得这里指从内存中恢复)这些类型的上下文。恢复上下文将产生一些开销,比如,一个程序在使用模拟(之前的模拟身份之类的)的时候并大量使用async将会变得更慢一些。我建议必变.NET创建上下文的功能,除非你认为这真的有必要。

await能用在哪儿?

  await可以用在任何标记async的方法和和方法内大部分的地方,但是有一些地方你不能用await。我将解释为什么在某些情况下不允许await。

catch和finally块

  虽然在try块中使用await是完全允许的,但是他不允许在catch和finally块中使用。通常在catch和finall块中,异常依然在堆栈中未解决的状态,并且之后将会被抛出。如果await在这个时刻前使用,栈将会有所不同,并且抛出异常的行为将会变得难以定义。

  请记住替代在catch块中使用block的方法是在其后面,通过返回一个布尔值来记录操作是否抛出一个异常。示例如下:

try
{
   page = await webClient.DownloadStringTaskAsync("http://oreilly.com");
}
catch (WebException)
{
   page = await webClient.DownloadStringTaskAsync("http://oreillymirror.com");
}

   你可以以如下方式替代:

bool failed = false;
try
{
   page = await webClient.DownloadStringTaskAsync("http://oreilly.com");
}
catch (WebException)
{
   failed = true;
}
if (failed)
{
   page = await webClient.DownloadStringTaskAsync("http://oreillymirror.com");
}

  lock块

  lock是一种帮助编程人员防止其它线程和当前线程访问相同对象的方式。因为异步代码通常会释放开始执行异步的线程,并且会被回调并且发生回调在一个不确定的时间量之后,即被释放掉后和开始的线程不同(译者:即使相同的线程,它也是释放掉之后的了),所以在await上加锁没有任何意义。

   在一些情况下,保护你的对象不被并发访问是很重要的,但是在没有其他线程在await期间来访问你的对象,使用锁是没有必要的。在这些情况下,你的操作是有些冗余的,显式地锁定了两次,如下:

lock (sync)
{
    // Prepare for async operation
}
    int myNum = await AlexsMethodAsync();
lock (sync)
{
    // Use result of async operation
}

  另外,你可以使用一个类库来进行处理并发控制,比如NAct,我们将会在第十章介绍

  如果你不够幸运,你可能需要在执行异步操作时保持某种锁。这时,你就需要苦思冥想并小心谨慎,因为通常锁住异步调用资源,而不造成争用和死锁是非常困难的。也许遇到这种情况想其他办法或者重构你的程序是最好的选择。

  Linq Query表达式

  C#有一种语法帮助我们更加容易的去通过书写querys来达到过滤,排序,分组等目的。这些query可以被执行在.NET平台上或者转换成数据库操作甚至其他数据源操作。

IEnumerable<int> transformed = from x in alexsInts
where x != 9
select x + 2;

  C#是在大多数位置是不允许在Query表达式中使用await关键字的。是因为这些位置会被编译成lambda表达式,正因为如此,该lambda表达式需要标记为async关键字。只是这样含蓄的lambda表达式不存在,即使如果真的这样做也会让人confuse。

  我们还是有办法,你可以写当量的表达式,通过使用Linq内部带的拓展方法。然后lambda表达式变得明了可读,继而你也就可以标记他们为async,从而使用await了。(译者:请对照上下代码来阅读)

IEnumerable<Task<int>> tasks = alexsInts
.Where(x => x != 9)
.Select(async x => await DoSomthingAsync(x) + await DoSomthingElseAsync(x));
IEnumerable<int> transformed = await Task.WhenAll(tasks);

  为了收集结果,我使用了Task.WhenAll,这是为Task集合所工作的工具,我将会在第七章介绍细节。

  不安全(unsafe)的代码

  代码被标记为unsafe的不能包含await,非安全的代码应该做到非常罕见并且应该保持方法独用和不需要异步。反正在编译器对await做转换的时候也会跳出unsafe代码。(译者:我觉得其实这里不用太在意啦,反正没写过unsafe关键字的代码)

捕获异常

  异步方法的异常捕获被微软设计的尽量和我们正常同步代码一样的。然而异步的复杂性意味着他们之间还会有些细微差别。在这里我将介绍异步如何简单的处理异常,我也将在第九章详细讲解注意事项。

  当耗时操作结束时,Task类型会有一个概念来表明成功还是失败。最简单的就是由IsFaulted属性来向外暴露,在执行过程中发生异常它的值就是true。await关键字将会察觉到这一点并且会抛出Task中包含的异常。

            如果你熟悉.NET异常机制,用也许会担心异常的堆栈跟踪在抛出异常时如何正确的保存。这在过去也许是不可能的。然而在.NET4.5中,这个限制被修改掉了,通过一个叫做ExceptionDispatchInfo的类,即一个协作异常的捕捉,抛出和正确的堆栈跟踪的类。

  异步方法也能察觉到异常。在执行异步方法期间发生任何异常,都不会被捕捉,他们会随着Task的返回而返回给调用者。当发生这种情况时,如果调用者在await这个Task,那么异常将会在此处抛出。(译者:之前有讲到异常在异步中会被传递)。在这种方式下,异常通过调用者传播,会形成一个虚拟的堆栈跟踪,完全就像它发生在同步代码中一样。

            我把它乘坐虚拟堆栈跟踪,因为堆栈是一个单线程拥有的这样的概念,并且在异步代码中,当前线程实际的堆栈和产生异常那个线程的堆栈可能是非常不同的。异常捕捉的是用户意图中的堆栈跟踪,而不是C#如何选择执行这些方法的细节。

直到被需要前异步方法都是同步的

  我之前说的,使用await只能消费(调用)异步方法。直到await结果发生,这个调用方法的语句在调用他们的线程中运行,就像同步方法一样。这非常具有现实意义,尤其是以一个同步的过程完成所有异步方法链时。(译者:当使用await的时候,的确就是按照同步的顺序来执行)

  还记得之前异步方法暂停在第一次遇到await时。即使这样,它有时候也不需要暂停,因为有时await的Task已经完成了。一个Task已经被完成的情况如下:

   ·他是被创建完成的,通过Task.FromResult工具方法。我们将会在第七章详细探讨。

   ·由没遇到async的async方法返回。

   ·它运行一个真正的异步操作,但是现在已经完成了(很可能是由于当前线程在遇到await之前已经做了某些事情)。

   ·它被一个遇到await的asunc方法返回,但是所await的这个之前就已经完成了。

  由于最后一个可能性,一些有趣的事情发生在你await一个已经完成的Task,很可能是在一个深度的异步方法链中。整个链很像完全同步的。这是因为在异步方法链中,第一个await被调用的方法总是异步链最深的一个。其他的方法到达后,最深的方法才有机会返回。( The others are only reached after the deepest method has had a chance to return synchronously.译者:按照语法来讲我的这句话貌似翻译的不正确,但是我个人觉得实际情况就是我说的这个样子。在遇到第一个await后,后面异步方法链中的await依次执行,逐个返回,最后才返回结果到最深的方法,也就是第一个方法,有高人来提出这里的见解吗?)

   你也许会怀疑为什么在第一种或第二种情况下还使用async。如果这些方法承诺一直同步的返回,你是正确的,并且这样写同步的代码效率高于异步并且没有await的过程。然后,这只是方法同步返回的情况。比如,一个方法缓存其结果到内存中,并在缓存可用的时候,结果可以被同步地返回,但是当它需要异步的网络请求。当你知道有一个好机会让你使用异步方法,在某种程度上你也许还想要方法返回Task或者Task<T>。(异步:既然方法链中有一个要异步,那么就会影响整体都使用异步)。

写在最后

  关于异步我还有很多疑惑,也是随着文章逐步明白,我也希望能快一点啊。

posted @ 2016-07-11 00:27  坦荡  阅读(3323)  评论(8编辑  收藏  举报