如何用 C# 写一个好的异步方法
用C#设计一个好的异步方法:
- 它应该有尽量少的参数,甚至不要参数。如果可能的话一定要避免
ref
和out
参数。 - 如果有意义的话,他应该有一个返回类型,他能真正的表达方法代码的结果,而不是像 C++ 那种成功标识。
- 它应该有一个可以解释自己行为的命名,而不依赖于额外的符号或注释。
使用Async void
是一大禁忌:
Async void
方法中抛出的异常无法通过外面的方法捕获
当 async Task
或 async Task<T>
方法中抛出异常,这个异常会被捕获并且放置到 Task 对象中。而 Async void
中没有 Task 对象,所以 Async void
抛出任何异常都将直接在启动异步 void 方法时处于活动状态的 SynchronizationContext
上引发。
SynchronizationContext
SynchronizationContext
可以使一个线程与另一个线程进行通信。假设你有两个线程,Thead1 和 Thread2。Thread1 做某些事情,然后它想在 Thread2 里面执行一些代码。一个可以实现的方式是:请求 Thread 得到 SynchronizationContext
这个对象,把它给 Thread1,然后 Thread1 可以调用 SynchronizationContext
的 send 方法在 Thread2 里面执行代码。
不是每一个线程都有一个 SynchronizationContext
对象。一个总是有 SynchronizationContext
对象的是 UI 线程。UI 线程中的控件被创建的时候会把 SynchronizationContext
对象放到这个线程中。
SynchronizationContext.Current
对象不是一个 AppDomain
一个实例的,而是每个线程一个实例。这就意味着两个线程在调用Synchronization.Current
时将会拥有他们自己的 SynchronizationContext
对象实例。Context
上下文存储在线程data store
(不是在 appDomain 的全局内存空间)。
SynchronizationContext
中有 send、post 静态方法。
```C# public async void AsyncVoidMethodThrowsException() { throw new Exception("Hmmm, something went wrong!"); }
public void ThisWillNotCatchTheException() { try { AsyncVoidMethodThrowsException(); } catch(Exception ex) { //The below line will never be reached Debug.WriteLine(ex.Message); } }
public async Task AsyncTaskMethodThrowsException() { throw new Exception("Hmmm, something went wrong!"); }
public async Task ThisWillCatchTheException() { try { await AsyncTaskMethodThrowsException(); } catch (Exception ex) { //The below line will actually be reached Debug.WriteLine(ex.Message); } } ```
Async void
方法很难被测试
返回 Task
而不是返回 await
```C#
public async Task
public Task
```
因为每次将一个方法声明为 async
时,编译器都会创建一个状态机类来封装这个方法逻辑,这增加了一定量的开销。如果这个方法不需要异步,而是返回一个 Task<T>
,让其他合适的地方处理它, 使方法的返回类型为 Task<T>
(而不是 async T
),这样就避免了状态机的生成,从而使代码更简洁,耗时更少。
但是,凡事都有例外,如果返回一个 Task<T>
,那么返回将立即发生,因此,如果代码在 try/catch
块中,则不会捕获异常。类似地,如果代码在 using
块中,它将立即释放对象。
```C#
Task
//可以正常工作
async Task
```
避免使用 .Wait()
或者 .Result
,使用.GetAwaiter().GetResult()
代替
使用.Wait()
或者.Result
有在 GUI 应用程序中发生死锁
默认中,当一个未完成的 Task
被await
的时候,当前的上下文将会在该Task
完成的时候重新获得并继续执行剩余的代码。这个context
就是当前的SynchronizationContext
,除非它是空的。GUI应用程序的SynchronizationContext
有排他性,只允许一个线程运行。
```C# public class DeadlockClass { private static async Task DelayAsync() { await Task.Delay(1000); }
// 当这个方法访问界面元素时,哈哈,死锁就出来了 public static void Test() { // 开始delay var delayTask = DelayAsync(); // 等待Delay的结束 delayTask.Wait(); } }
private void TextButton_OnClick(object sender, RoutedEventArgs e) { DeadlockClass.Test(); TextButton.Content = "Helius"; } ```
在 UI 线程上调用DeadlockClass.Test()
,当 await
完成的时候,它试图在它原来的代码上下文(UI线程)执行它剩余的部分,但是该代码上下文已经有一个线程在了,就是那个一直在同步等待 async
完成的那个线程,它们两个相互等待,然后就死锁了。
另外一点需要注意的是,控制台并不会导致死锁,控制台的SynchronizationContext
类似于一个线程池的机制而不是排他的。因此当await
结束的时候,它可以重新获得原来的上下文然后执行完剩余代码。这个不同是很多人产生困惑的根源,当他们在控制台测试的时候程序还OK,用GUI程序跑就发生了死锁。
使用.Wait()
或者 .Result
将会将异常封装在AggregateException
中,这会增加错误处理的复杂性。
```C#
class Program { //MainAsync 中的 try/catch 会捕获特定异常类型,但是如果将 try/catch 置于 Main 中,则它会始终捕获 AggregateException。 static void Main(string[] args) { Console.WriteLine($"{Thread.CurrentThread.ManagedThreadId}"); MainAsync().Wait(); Console.ReadKey(); }
static async Task MainAsync() { try { await Task.Delay(1000); Console.WriteLine($"{Thread.CurrentThread.ManagedThreadId}"); } catch (Exception ex) { } } } ```
高级的.GetAwaiter().GetResult()
将返回一个常规的异常
```C#
public void GetAwaiterGetResultExample() { //This is ok, but if an error is thrown, it will be encapsulated in an AggregateException string data = GetData().Result; //This is better, if an error is thrown, it will be contained in a regular Exception data = GetData().GetAwaiter().GetResult(); } ```
异步库方法应该考虑使用Task.ConfigureAwait(false)
来提高性能、避免死锁
随着异步 GUI 应用程序使用,可能会发现async
方法的许多小部件都在使用GUI线程作为其上下文。这可能会形成迟滞。 ConfigureAwait
可以提高性能 。
```C#
public class ConfigureAwaitFalse
{
async Task MyMethodAsync()
{
//这里还是调用线程的上下文(context)
await Task.Delay(1000);
//这里还是调用线程的上下文
await Task.Delay(1000).ConfigureAwait(continueOnCapturedContext: false);
//这里的上下文就不会是调用线程的上下文了,而是随机的上下文
//do Something
}
}
``
上面的代码与注释已经非常明显了。所以有一点是需要注意的,就是如果方法中在
await后还需要用到上下文的,就不能设置
ConfigureAwait(false),比如GUI程序中,如果方法中
await`后还有一些界面元素的操作,就会抛出线程异常了。
```C#
private async void TextButton_OnClick(object sender, RoutedEventArgs e) { TextButton.IsEnabled = false; try { await Task.Delay(2000).ConfigureAwait(false); } finally { TextButton.IsEnabled = true;//这里会抛出异常 } } ```
可以修改为: ```C# private async void TextButton_OnClick(object sender, RoutedEventArgs e) { TextButton.IsEnabled = false; try { //await Task.Delay(2000).ConfigureAwait(false); await HandleClickAsync(); } finally { TextButton.IsEnabled = true; } }
private async Task HandleClickAsync()
{
await Task.Delay(2000).ConfigureAwait(false);
}
``
Task 完成后,
synchronization context(同步上下文)将调用
post()方法恢复到原来的位置。 但是,在编写库代码时,很少需要返回到以前的上下文。当使用
task.configureawait(false)` 时,代码将不再尝试恢复到以前的位置。这稍微提高了性能,并有助于避免死锁。