(译)关于async与await的FAQ

2019年11月19日 阅读数:58
这篇文章主要向大家介绍(译)关于async与await的FAQ,主要内容包括基础应用、实用技巧、原理机制等方面,希望对大家有所帮助。

传送门:异步编程系列目录……html

         环境:VS2012(尽管System.Threading.Tasks.net4.0就引入,在.net4.5中为其增长了更丰富的API及性能提高,另外关键字”async””await”是在C#5.0引入的。vs2010打 Visual Studio Async CTP for VS2010补丁能够引入关键字”async””await”的支持,可是得不到.net4.5新增API的支持)编程

      (CTP:Community Test Preview 社区测试试用版,就是通常的测试版本)异步

术语:async

APM           异步编程模型,Asynchronous Programming Modelide

EAP           基于事件的异步编程模式,Event-based Asynchronous Pattern异步编程

TAP           基于任务的异步编程模式,Task-based Asynchronous Pattern函数

         我经常收到来自开发人员的一些问题,这些问题主要集中在C#Visual Basic中的新关键字”async””await”。我已经将这些问题分类整理,并借此机会分享给你们。post

 

概念概述性能

1.         从哪能得到关于”async””await”主题的优秀资源?测试

一般,你能在Visual Studio Async主题中找到不少资源(eg:文章、视频、博客等等)。201110月份的MSDN杂志包含了三篇介绍”async””await”主题的优秀文章。若是你阅读,我推荐你阅读顺序依次为:

1) 《经过新的 Visual Studio Async CTP 更轻松地进行异步编程》

2) 《经过 Await 暂停和播放》

3) 《了解 Async 和 Await 的成本》

.NET团队博客一样也有”async””await”主题的优秀资源:Async in .NET4.5: 值得期待

2.         为何须要编译器帮助咱们完成异步编程?

Anders Hejlsberg’s2011 微软Build大会上花了1个小时来帮咱们说明为何编译器在这里真的有用,视频:C#Visual Basic将来的发展方向》。简而言之,传统的异步编程模型(APMEAP)要求你手写大量代码(eg:连续传递委托、回调)来实现,而且这些代码会致使语句控制流混乱颠倒。经过.NET4.5提供的新的编程模型(TAP),你能够像在写同步代码同样使用常规的顺序控制流结合并行任务及”async””await”关键字来完成异步编程,编译器在后台应用必要的转换以使用回调方式来避免阻塞线程。

3.         经过Task.Run() 将同步方法包装成异步任务是否真的有益处?

这取决于你的目标,你为何要异步调用方法。若是你的目标只是想将当前任务切换到另外一个线程执行,好比,保证UI线程的响应能力,那么确定有益。若是你的目标是为了提升可扩展性,那么使用Task.Run() 包装成异步调用将没有任何实际意义。更多信息,请看《我是否应该公开同步方法对应的异步方法API?》。经过Task.Run() 你能够很轻松的实现从UI线程分担工做到另外一个工做线程,且可协调后台线程一旦完成工做就返回到UI线程。(这里说的可扩展性就如当增长cpu时,Task.Run()并不会增长程序的并行效率,由于他只至关于启动了一个线程执行任务,假若使用Parallel.For就具备更好的可扩展性。什么是系统的可扩展性?

 

“async”关键字

1.         将关键字”async”应用到方法上的做用是什么?

当你用关键字”async”标记一个方法时,即告诉了编译器两件事:

1)         你告诉编译器,想在方法内部使用”await”关键字(只有标记了”async”关键字的方法或lambda表达式才能使用”await”关键字)。这样作后,编译器会将方法转化为包含状态机的方法(相似的还有yield的工做原理,请看 C#稳固基础:传统遍历与迭代器 ),编译后的方法能够在await处挂起而且在await标记的任务完成后异步唤醒

2)         你告诉编译器,方法的结果或任何可能发生的异常都将做为返回类型返回。若是方法返回TaskTask<TResult>,这意味着任何结果值或任何在方法内部未处理的异常都将存储在返回的Task中。若是方法返回void,这意味着任何异常会被传播到调用者上下文。

a)         async void函数只能在UI Event回调中使用。

b)         async void函数中必定要用try-catch捕获全部异常,不然会很容易致使程序崩溃。另外须要特别注意lambda表达式,

如:(List<T> 只有 public void ForEach(Action<T> action); 重载)

Enumerable.Range(0, 3).ToList().ForEach(async (i) => { throw new Exception(); });

这段代码就隐式生成了async void 函数,直接致使了程序的crash

         不过好在,编译器是优先考虑生成 async Task 形式的匿名函数的。即以下两个重载,编译器是使用ForEach(Func<T, Task> action);重载生成async Task 函数。

public void ForEach(Action<T> action);
public void ForEach(Func<T, Task> action);

c)         注册TaskScheduler.UnobservedTaskException事件,记录Task中未处理异常信息,方便分析及错误定位。

2.         ”async”关键字标记的方法的调用都会强制转变为异步方式吗?

不会,当你调用一个标记了”async”关键字的方法,它会在当前线程以同步的方式开始运行。因此,若是你有一个同步方法,它返回void而且你作的全部改变只是将其标记的”async”,这个方法调用依然是同步的。返回值为TaskTask<TResult>也同样

方法用”async”关键字标记不会影响方法是同步仍是异步运行并完成,而是,它使方法可被分割成多个片断,其中一些片断可能异步运行,这样这个方法可能异步完成。这些片断界限就出如今方法内部显示使用”await”关键字的位置处。因此,若是在标记了”async”的方法中没有显示使用”await”,那么该方法只有一个片断,而且将以同步方式运行并完成。

3.         “async”关键字会致使调用方法被排队到ThreadPool吗?会建立一个新的线程吗?

全都不会,”async”关键字指示编译器在方法内部可能会使用”await”关键字,这样该方法就能够在await处挂起而且在await标记的任务完成后异步唤醒。这也是为何编译器在编译”async” 标记的方法时,方法内部没有使用”await”会出现警告的缘由warning CS4014: 因为不等待此调用,所以会在此调用完成前继续执行当前方法。请考虑向此调用的结果应用"await"运算符)

4.         ”async”关键字能标记任何方法吗?

不能,只有返回类型为voidTaskTask<TResult>的方法才能”async”标记。而且,并非全部返回类型知足上面条件的方法都能用”async”标记。以下,咱们不容许使用”async”标记方法:

1)         在程序的入口方法(egMain()),不容许。当你正在await的任务还未完成,但执行已经返回给方法的调用者了。EgMain(),这将退出Main()直接致使退出程序。

2)         在方法包含以下特性时,不容许。

l  [MethodImpl(MethodImplOptions.Synchronized)]

为何这是不容许的,详细请看What’s New for Parallelism in .NET 4.5 Beta。此特性将方法标记为同步相似于使用lock/SyncLock同步基元包裹整个方法体。

l  [SecurityCritical][SecuritySafeCritical]   (Critical:关键)

         编译器在编译一个”async”标记的方法,原方法体实际上最终被编译到新生成的MoveNext()方法中,可是其标记的特性依然存在。这意味着特性如[SecurityCritical]不会正常工做。

3)         在包含refout参数的方法中,不容许。调用者指望方法同步调用完成时能确保设置参数值,可是标记为”async”的方法可能不能保证马上设置参数值直到异步调用完成。

4)         Lambda被用做表达式树时,不容许。异步lambda表达式不能被转换为表达式树。

5.         是否有任何约定,这时应该使用”async”标记方法?

有,基于任务的异步编程模型(TAP)是彻底专一于怎样实现异步方法,这个方法返回TaskTask<TResult>。这包括(但不限于)使用”async””await”关键字实现的方法。想要深刻TAP,请看《基于任务的异步编程模型》文档。

6.         “async”标记的方法建立的Tasks是否须要调用”Start()”

不须要,TAP方法返回的Tasks是已经正在操做的任务。你不只不须要调用”Start()”,并且若是你尝试也会失败。更多细节,请看《.NET4.X 并行任务中Task.Start()的FAQ

7.         “async”标记的方法建立的Tasks是否须要调用”Dispose()”

不须要,通常来讲,你不须要 Dispose() 任何任务。请看《.NET4.X并行任务Task须要释放吗?》

8.         “async”是如何关联到当前SynchronizationContext

对于”async” 标记的方法,若是返回TaskTask<TResult>,则没有方法级的SynchronizationContext交互;对于”async” 标记的方法,若是返回void,则有一个隐藏的SynchronizationContext交互。

当一个”async void”方法被调用,方法调用的开端将捕获当前SynchronizationContext(“捕获在这表示访问它而且将其存储)。若是这里有一个非空的SynchronizationContext,将会影响两件事:(前提:”async void”

1)         在方法调用的开始将致使调用捕获SynchronizationContext.OperationStarted()方法,而且在完成方法的执行时(不管是同步仍是异步)将致使调用捕获SynchronizationContext.OprationCompleted()方法。这给上下文引用计数未完成异步操做提供时机点。若是TAP方法返回TaskTask<TResult>,调用者可经过返回的Task作到一样的跟踪。

2)         若是这个方法是由于未处理的异常致使方法完成,那么这个异常将会提交给捕获的SynchronizationContext。这给上下文一个处理错误的时机点。若是TAP方法返回TaskTask<TResult>,调用者可经过返回的Task获得异常信息。

当调用”async void”方法时若是没有SynchronizationContext,没有上下文被捕获,而后也不会调用OperaionStarted/OperationCompleted方法。在这种状况下,若是存在一个未处理过的异常在ThreadPool上传播,那么这会采起线程池线程默认行为,即致使进程被终止。

 

“await”关键字

1.         “await”关键字作了什么

“await”关键字告诉编译器在”async”标记的方法中插入一个可能的挂起/唤醒点。

         逻辑上,这意味着当你写”await someObject;”时,编译器将生成代码来检查someObject表明的操做是否已经完成。若是已经完成,则从await标记的唤醒点处继续开始同步执行;若是没有完成,将为等待的someObject生成一个continue委托,当someObject表明的操做完成的时候调用continue委托。这个continue委托将控制权从新返回到”async”方法对应的await唤醒点处。

返回到await唤醒点处后,无论等待的someObject是否已经经完成,任何结果均可从Task中提取,或者若是someObject操做失败,发生的任何异常随Task一块儿返回或返回给SynchronizationContext。

         在代码中,意味着当你写:

         await someObject;

         编译器会生成一个包含 MoveNext 方法的状态机类:

private class FooAsyncStateMachine : IAsyncStateMachine
{ 
    // Member fields for preserving “locals” and other necessary     state 
    int $state; 
    TaskAwaiter $awaiter; 
    … 
    public void MoveNext() 
    { 
        // Jump table to get back to the right statement upon         resumption 
        switch (this.$state) 
        { 
            … 
        case 2: goto Label2; 
            … 
        } 
        … 
        // Expansion of “await someObject;” 
        this.$awaiter = someObject.GetAwaiter(); 
        if (!this.$awaiter.IsCompleted) 
        { 
            this.$state = 2; 
            this.$awaiter.OnCompleted(MoveNext); 
            return; 
            Label2: 
        } 
        this.$awaiter.GetResult(); 
        … 
    } 
}

在实例 someObject上使用这些成员来检查该对象是否已完成(经过 IsCompleted),若是未完成,则挂接一个续体(经过 OnCompleted),当所等待实例最终完成时,系统将再次调用 MoveNext 方法,完成后,来自该操做的任何异常将获得传播或做为结果返回(经过 GetResult),并跳转至上次执行中断的位置。

2.         什么是”awaitables”?什么是”awaiters”

         虽然TaskTask<TResult>是两个很是广泛的等待类型(“awaitable”),但这并不表示只有这两个的等待类型。

“awaitable”能够是任何类型,它必须公开一个GetAwaiter() 方法而且返回有效的”awaiter”。这个GetAwaiter() 多是一个实例方法(eg:TaskTask<TResult>的实例方法),或者多是一个扩展方法。

“awaiter””awaitable”对象的GetAwaiter()方法返回的符合特定的模式的类型。”awaiter”必须实现System.Runtime.CompilerServices.INotifyCompletion接口(,并可选的实现System.Runtime.CompilerServices.ICriticalNotifyCompletion接口)。除了提供一个INotifyCompletion接口的OnCompleted方法实现(,可选提供ICriticalNotifyCompletion接口的UnsafeCompleted方法实现),还必须提供一个名为IsCompletedBoolean属性以及一个无参的GetResult()方法。GetResult()返回void,若是”awaitable”表明一个void返回操做,或者它返回一个TResult,若是”awaitable”表明一个TResult返回操做。

几种方法来实现自定义的”awaitable” 谈论,请看await anything。也能针对特殊的情景实现自定义”awaitable”,请看Advanced APM Consumption in Async MethodsAwaiting Socket Operations

3.         哪些地方不能使用”await”

1)         在未标记”async”的方法或lambda表达式中,不能使用”await””async”关键字告诉编译器其标记的方法内部可使用”await”。(更详细,请看Asynchrony in C# 5 Part Six: Whither async?

2)         在属性的gettersetter访问器中,不能使用”await”。属性的意义是快速的返回给调用者,所以不指望使用异步,异步是专门为潜在的长时间运做的操做。若是你必须在你的属性中使用异步,你能够经过实现异步方法而后在你的属性中调用。

3)         lock/SyncLock块中,不能使用”await”。关于谈论为何不容许,以及SemaphoreSlim.WaitAsync(哪个能用于此状况的等待),请看What’s New for Parallelism in .NET 4.5 Beta。你还能够阅读以下文章,关于如何构建各类自定义异步同步基元:

a)         构建Async同步基元,Part 1 AsyncManualResetEvent

b)         构建Async同步基元,Part 2 AsyncAutoResetEvent

c)         构建Async同步基元,Part 3 AsyncCountdownEvent

d)         构建Async同步基元,Part 4 AsyncBarrier

e)         构建Async同步基元,Part 5 AsyncSemaphore

f)          构建Async同步基元,Part 6 AsyncLock

g)         构建Async同步基元,Part 7 AsyncReaderWriterLock

4)         unsafe区域中,不能使用”await”。注意,你能在标记”async”的方法内部使用”unsafe”关键字,可是你不能在unsafe区域中使用”await”

5)         catch块和finally块中,不能使用”await”。你能在try块中使用”await”,无论它是否有相关的catch块和finally块,可是你不能在catch块或finally块中使用”await”。这样作会破坏CLR的异常处理。

6)         LINQ中大部分查询语法中,不能使用”await””await”可能只用于查询表达式中的第一个集合表达式的”from”子句或在集合表达式中的”join”子句。

4.         “await task;””task.Wait”效果同样吗?

不。

“task.Wait()”是一个同步,可能阻塞的调用。它不会马上返回到Wait()的调用者,直到这个任务进入最终状态,这意味着已进入RanToCompletionFaulted,或Canceled完成状态。相比之下,”await task;”告诉编译器在”async”标记的方法内部插入一个隐藏的挂起/唤醒点,这样,若是等待的task没有完成,异步方法也会立马返回给调用者,当等待的任务完成时唤醒它从隐藏点处继续执行。当”await task;”会致使比较多应用程序无响应或死锁的状况下使用“task.Wait()”。更多信息请看Await, and UI, and deadlocks! Oh my!

                   当你使用”async””await”时,还有其余一些潜在缺陷。Eg

1)         避免传递lambda表达式的潜在缺陷

2)         保证”async”方法不要被释放

3)         不要忘记完成你的任务

4)         使用”await”依然可能存在死锁

5.         “task.Result””task.GetAwaiter().GetResult()”之间存在功能区别吗?

存在。但仅仅在任务以非成功状态完成的状况下。若是task是以RanToCompletion状态完成,那么这两个语句是等价的。然而,若是task是以FaultedCanceled状态完成,task.Result将传播一个或多个异常封装而成的AggregateException对象;而”task.GetAwaiter().GetResult()”将直接传播异常(若是有多个任务,它只会传播其中一个)。关于为何会存在这个差别,请看.NET4.5中任务的异常处理》

6.         “await”是如何关联到当前SynchronizationContext

这彻底取决于被等待的类型。对于给定的”awaitable”,编译器生成的代码最终会调用”awaiter”OnCompleted()方法,而且传递将执行的continue委托。编译器生成的代码对SynchronizationContext一无所知,仅仅依赖当等待的操做完成时调用OnCompleted()方法时所提供的委托。这就是OnCompleted()方法,它负责确保委托在正确的地方被调用,正确的地方彻底由”awaiter”决定。

正在等待的任务(TaskTask<TResult>GetAwaiter方法分别返回的TaskAwaiterTaskAwaiter<TResult>类型)的默认行为是在挂起前捕获当前的SynchronizationContext,而后等待task的完成,若是能捕获到当前的SynchronzationContext,调用continue委托将控制权返回到SynchronizationContext中。因此,例如,若是你在应用程序的UI线程上执行”await task;”,若是当前SynchronizationContext非空则将调用OnCompleted(),而且在任务完成时,将使用UISynchronizationContext传播continue委托返回到UI线程。

当你等待一个任务,若是没有当前SynchronizationContext,那么系统会检查当前的TaskScheduler,若是有,当task完成时将使用TaskScheduler调度continue委托。

若是SynchronizationContextTaskScheduler都没有,没法迫使continue委托返回到原来的上下文,或者你使用”await task.ConfigureAwait(false)代替”await task;”,而后continue委托不会迫使返回到原来上下文而且将容许在系统认为合适的地方继续运行。这一般意味着要么以同步方式运行continue委托不管等待的task在哪完成;要么使用ThreadPool中的线程运行continue委托。

7.         在控制台程序中能使用”await”吗?

固然能。但你不能在Main()方法中使用”await”,由于入口点不能被标记为”async”。相反,你能在控制台应用程序的其余方法中使用”await”。若是你在Main()中调用这些方法,你能够同步等待(而不是异步等待)他们的完成。Eg

         你还可使用自定义的SynchronizationContextTaskScheduler来实现类似的功能,更多信息请看:

1)         Await, SynchronizationContext, and Console Apps: Part 1

2)         Await, SynchronizationContext, and Console Apps: Part 2

3)         Await, SynchronizationContext, and Console Apps: Part 3

8.         “await”能和异步编程模型模式(APM)或基于事件的异步编程模式(EAP)一块儿使用吗?

固然能,你能够为你的异步操做实现一个自定义的”awaitable”,或者将你现有的异步操做转化为现有的”awaitable”,像tasktask<TResult>。示例以下:

1)         Tasks and the APM Pattern

2)         Tasks and the Event-based Asynchronous Pattern

3)         Advanced APM Consumption in Async Methods

4)         Implementing a SynchronizationContext.SendAsync method

5)         Awaiting Socket Operations

6)         await anything

7)         The Nature of TaskCompletionSource<TResult>

9.         编译器对async/await生成的代码是否能高效异步执行?

大多数状况下,是的。由于大量的生成代码已经被编译器所优化而且.NET Framework也为生成代码创建依赖关系。要了解更多信息,包括使用async/await的最小化开销的最佳实践等。请看

1)         .NET4.5TPL的性能提高

2)         2012MVP峰会上的“The Zen of Async”

3)         了解 Async 和 Await 的成本

 

 

原文:http://blogs.msdn.com/b/pfxteam/archive/2012/04/12/async-await-faq.aspx

做者:Stephen Toub - MSFT

另外,稍做改动,参考文献:C# Async Tips and Tricks Part 2 : Async Void