异步编程
现代异步.net应用程序用到了两个关键字,分别是async和await。在声明方法时,会添加async关键字,它能发挥两重用途:激活该方法中的await关键字,并知会编译器为该方法生成一个状态机,类似于yield return的工作方式。如果async方法能发挥一个值,那么这个值可能是Task<TResult>。如果不能返回值或者其他类似于task的类型,比如ValueTask,那么他便会返回Task。此外,若async方法以一组枚举返回了多个值,他便会返回IAsyncEnumerable<T>或IAsyncEnumerator<T>。这些类似于task的类型都代表future,这些future则会在async方法完成时通知调用代码。
注意:切户使用async void!只有在编写async实践处理程序时,才需要使用async方法来返回void.无返回值的常规async方法应当返回Task,而非void
async Task DoSomethingAsync()
{
int value=13;
//异步等待1秒
await Task.Delay(TimeSpan.FromSeconds(1));
value*=2;
//异步等待两秒
await Task.Delay(TimeSpan.FromSeconds(1));
Trace.WriteLine(value);
}
与其他方法一样,async方法一开始同步执行。在async方法中,await关键字会对其实际参数执行异步等待。异步等待会检查操作是否已经完成。若已完成,他便会继续同步执行。否则,他会让async方法暂停并返回一个不完整的任务。当随后操作完成时,async方法会恢复执行。
可以把async方法当做若干个由await语句分割而成的同步部分。哪一个线程调用方法,第一个同步部分就在哪一个线程中执行。但是其他的同步部分在哪儿执行呢?答案有一点复杂。
以最常见的场景为例,当等待某个任务时,如果await决定暂停方法,那么这时会捕捉到一个上下文(context)。如果不为null,它便是当前的synchronizationContext。但如果是null,上下文则为当前的TaskScheduler。无论哪种情况,方法都会在被捕捉的上下文中继续执行。通常,如果当前环境是UI线程,那么这个上下文是UI上下文;如果是其他环境,则往往会使线程池上下文。
在上面代码中,所有的同步部分都会试图在原先的上限为中继续运行。如果从UI线程中调用DoSomethingAsync,它的每一个同步部分都将在该UI线程中运行。如果从线程池的某个线程中调用它,那么这些同步部分将在线程池的任意线程中运行。
通过异步等待ConfigureAwait扩展方法的结果,并将false作为continueOnCapturedContext参数的值传入,可以规避这种默认的行为。下面的代码从调用线程开始,当被await暂停后,它会在线程池中继续运行。
async Task DoSomethingAsync()
{
int value=13;
//异步等待1秒
await Task.Delay(TimeSpan.FromSeconds(1)).ConfigureAwait(false);
value*=2;
//异步等待两秒
await Task.Delay(TimeSpan.FromSeconds(1)).ConfigureAwait(false);
Trace.WriteLine(value);
}
始终在核心”库”方法中调用ConfigureAwait,并且,仅当在外部“用户界面”方法中需要它时才恢复上下文,这是很好的做法
await关键字并不仅限于处理任务,它也可以处理任何符合特定格式的可等待对象。但是在大多数情况下,await处理的是Task或Task<TResult>。
可以通过两种方法来创建Task实例。
1. 某些任务代表的是CPU需要执行的实际代码,这是要调用Task.Run来创建这些运算型的任务
2. 要在某个特定的调度器上运行这些任务。需要调用Task.Factory.StartNew
3. 创建基于实践的任务应当采用TaskCompletionSource<TResult>(或者它的某种快捷方式)。大多数I/O任务采用了TaskCompletionSource<TResult>
异常处理
在下面代码中,PossibleExceptionAsync可能抛出NotSupportedException异常,但TrySomethingAsync会本能地捕捉异常。对于被捕捉的异常,其栈追踪会妥善地保留下来,而不会被认为地包装到TargetInvocationException或AggregateException中:
async Task TrySomethingAsync()
{
try
{
await PossibleExceptionAsync();
}
catch(NotSupportedException ex)
{
LogException(ex);
throw;
}
}
当async方法抛出或传播异常时,该异常被放入它返回的Task中,该Task就此完成。当该Task出在异步等待阶段时,await运算符会接收到该异常并将其再次抛出,期间,它的原始栈追踪会得以保留。因此,如果PossibleExceptionAsync是一种async方法,那么以下代码就会如预期那样工作:
async Task TrySomethingAsync()
{
//异常会在Task出现时终止,并不会被直接抛出
Task task=PossibleExceptionAsync();
try
{
//Task的异常会在此处发生
await task;
}
catch(NotSupportedException ex)
{
LogException(ex);
throw;
}
}
对于async方法,还有一条重要的准则:一旦开始使用async,最好任由它在代码中运行。如果调用async方法,终究要等待它返回的任务。尽量不要调用Task.Wait、Task<TResult>.Result或GetAwaiter().GetResult(),否则可能引发死锁:
async Task WaitAsync()
{
//该await会捕获当前的上下文
await Task.Delay(TimeSpan.FromSeconds(1));
//还会尝试在该上下文中继续执行这个方法
}
void Deadlock()
{
//开始延迟
Task task=WaitAsync();
//同步地阻塞并等待async方法完成;
task.Wait();
}
如果在UI上下文或ASP.NET Classic上下文中调用此例中的代码,便会引发死锁,因为这两种上下文一次都只允许一个线程。Deadlock会调用WaitAsync,引发延迟。随后,Deadlock会同步等待方法完成,并阻塞上下文线程。当延迟结束后,await会尝试在被捕获的上下文中恢复WaitAsync,但无法恢复,因为在上下文中已有一个受阻塞的线程,且该上限为一次只允许一个线程。
wait线程锁:主执行线程调用子线程后挂起等待子线程结果,子线程又需要切换到主线程或者等待主线程返回,从而导致两个线程均处在阻塞状态(死锁)
有两种方法能预防死锁:
1. 在WaitAsync中使用ConfigureAwait(false)(这样会让await忽视它的上下文)
2. 针对WaitAsync的调用使用await(把Deadlock放进某种async方法例里)。这样,异步任务后面继续执行的代码就不会回到原 UI 线程了,而是直接从线程池中再取出一个线程执行;这样,即便 UI 线程后续可能有别的原因造成阻塞,也不会产生死锁了。
一旦开始使用async,最好全程使用它