在上一篇《C#:异步编程中的 async 和 await》 中简单介绍了在 C# 中的异步编程以及 async 和 await 编程模型,本文介绍下异步编程的注意事项,主要有以下几个方面。
在同步代码中调用异步代码,容易导致死锁,所以在实际使用异步编程时,推荐的做法是一直异步到底。先来看一个会出现死锁的代码:
class Program
{
static void Main(string[] args)
{
while (true)
{
Task.Run(MethodSync);
Thread.Sleep(100);
}
}
static void MethodSync()
{
//string result =MethodAsync().Result;
MethodAsync().Wait();
}
static async Task<string> MethodAsync()
{
await Task.Run(() =>
{
Thread.Sleep(2000);
});
Console.WriteLine("MethodAsync End");
return "success";
}
}
运行上面代码,控制台会输出几次 MethodAsync End 后就会停止,这时死锁已经发生。可以观察到控制台程序使用的线程数会不断增加:
发生死锁的原因是:
只需要将 MethodSync 同步方法修改为异步就可以解决此问题:
static async Task MethodASync1()
{
await MethodAsync();
}
当然,有些时候我们需要在同步方法中调用异步方法,有下面两个方法:
因为上面的原因,所以我们在写代码时尽量不要在异步方法上返回 void ,但有两种情况也还是可以使用 void 返回值:
1、事件,比如在 Winform 程序中的按钮事件
private void btnTest_Click(object sender, EventArgs e)
{
await WriteLog();
}
如果要将 btnTest_Click 的返回值修改为 async Task ,编译时会报错。
2、记录日志之类的方法,或者说该方法执行的操作和主任务关系不大,无需知道处理的结果时。
当我们编写同步代码时,常用 try catch 来进行异常捕获,例如下面代码:
class Program
{
static void Main(string[] args)
{
try
{
TestException();
}
catch (Exception ex)
{
//TestException 方法抛出的异常会在这里被捕获
Console.WriteLine(ex.Message);
}
}
static void TestException()
{
throw new Exception("Test Exception");
}
}
同样的方式对异步方法进行 try catch ,会发现 catch 中的代码并没有执行:
class Program
{
static void Main(string[] args)
{
try
{
TestExceptionAsync();
}
catch (Exception ex)
{
//此处不会被调用
Console.WriteLine(ex.Message);
}
Console.WriteLine("main end");
Console.ReadLine();
}
static async Task TestExceptionAsync()
{
await Task.Delay(200);
throw new Exception("Test TestExceptionAsync");
}
}
要对异常方法进行异常捕获,必须使用 await 修饰符 、调用 Wait() 方法或者访问 Result 属性:
static async Task Main(string[] args)
{
try
{
//var result = TestExceptionAsync().Result;
//TestExceptionAsync().Wait();
await TestExceptionAsync();
}
catch (Exception ex)
{
Console.WriteLine(ex.Message);
}
Console.WriteLine("main end");
Console.ReadLine();
}
在异步方法的返回类型 Task 类中,有一个 Exception 属性,该属性返回的类型为 AggregateException ,而在 AggregateException 的内部又有一个 InnerExceptions 属性用来包装所有异常的集合。
对于使用 await 修饰符和调用 Wait() 方法、访问 Result 属性对于异常的捕获是有区别的:
当使用Wait 或 Result 的时候,异步方法是将自身的 AggregateException 对象往上抛,这样在异常处理的时候就会比较麻烦,我们需要这样来进行异常的解析:
static async Task Main(string[] args)
{
try
{
TestExceptionAsync().Wait();
}
catch (AggregateException aggregateException)
{
foreach (var ex in aggregateException.InnerExceptions)
{
Console.WriteLine(ex.Message);
}
}
Console.WriteLine("main end");
Console.ReadLine();
}
如果直接获取 aggregateException 的 Message 属性,则会输出:
One or more errors occurred. (Test TestExceptionAsync)
使用 await 修饰符,发生异常的时候,抛出的不是 AggregateException 对象,而是 AggregateException 对象中的 InnerExceptions 属性中找出第一个返回,随意在使用 await 修饰符的场景下,捕获异常的写法是符合我们编程习惯的。
static async Task Main(string[] args)
{
try
{
await TestExceptionAsync();
}
catch (Exception ex)
{
Console.WriteLine(ex.Message);
}
Console.ReadLine();
}