异步编程基础——async void方法的异常处理
问题
当有异常从async void 方法中传播出来时,需予以处理。
解决方案
如果可以,把方法的返回对象从void改为Task,不过在某些情况下,并不可行。比如需要对ICommand实现进行单元测试,此时必须返回void.在这种情况下,可以提供Execute方法的Task返回重载
sealed class MyAsyncCommand:ICommand
{
async void ICommand.Execute(object parameter)
{
await Execute(parameter);
}
public async Task Execute(object parameter)
{
...//此处为异步命令的实现
}
...//其他部分(CanExecute等)
}
最好避免让异常从async void方法中传播出去,如果必须使用async void 方法,就需要考虑将它的整个代码包装在try代码块中,直接处理异常。
关于async void方法汇总的异常处理,还有另一种方案。如果async void方法传播异常,在该方法开始执行的同事,一场会被抛到当时正在活动的SynchronizationContext上,如果执行环境提供了SynchronizationContext,那么通常有办法在全剧终处理这些顶层的异常,比如WPF有Application.DispatcherUnhandledException,UWP有Application.UnhandledException,ASP.NET则有UseExceptionHandler中间件。
此外,通过控制SynchronizationContext,也可以处理async void 方法中的异常。自行编写SynchronizationContext并非易事,但可以使用免费的Nito.AsyncExNuGet helper库中AsyncContext类型。对于没有内置SynchronizationContext的应用程序,比如Win32服务和控制台应用程序,AsyncContext尤为有用。下面的示例使用了AsyncContext来运行并处理async void 方法中的异常。
static class Program
{
static void Main(string[] args)
{
try
{
AsyncContext.Run(()=>MainAsync(args));
}
catch(Exception ex)
{
Console.Error.WriteLine(ex);
}
}
//这种代码很差劲!
//在实际应用中,若非必要,否则应避免使用async void
static async void MainAsync(string[] args)
{
...
}
}
讨论
返回Task的方法更容易测试,这也是async Task比async void更受青睐的原因之一,用返回Task的方法来重载返回void的方法,至少能体用可测试的API接口。
如果需要提供自己的SynchronizationContext类型(比如AsyncContext),切勿将SynchronizationContext放到任何并不为你所用的线程中。这是基本的准则,不应将此类型放到任何已经具备这种类型的线程中,比如UI线程或ASP.NET经典请求线程,也不应将此类型放到线程池线程中。但是,在控制台应用程序的主线程以及所有由你手动创建的现呈上都可以安装。
AsyncContext类型可以在Nito.AsyncEx NuGet包中获取