如何用 C# 写一个好的异步方法

语言: CN / TW / HK

用C#设计一个好的异步方法:

  • 它应该有尽量少的参数,甚至不要参数。如果可能的话一定要避免refout参数。
  • 如果有意义的话,他应该有一个返回类型,他能真正的表达方法代码的结果,而不是像 C++ 那种成功标识。
  • 它应该有一个可以解释自己行为的命名,而不依赖于额外的符号或注释。

使用Async void是一大禁忌:

Async void 方法中抛出的异常无法通过外面的方法捕获

async Taskasync 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 静态方法。

参考:搞懂SynchronizationContext

```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 AsyncTask() {     //Not great!     //...Non-async stuff happens here     //The await is the very last line of the code path - There is no continuation after it     return await GetData(); }

public Task JustTask() {     //Better!     //...Non-async stuff happens here     //Return a Task instead     return GetData(); }

```

因为每次将一个方法声明为 async 时,编译器都会创建一个状态机类来封装这个方法逻辑,这增加了一定量的开销。如果这个方法不需要异步,而是返回一个 Task<T>,让其他合适的地方处理它, 使方法的返回类型为 Task<T> (而不是 async T),这样就避免了状态机的生成,从而使代码更简洁,耗时更少。

但是,凡事都有例外,如果返回一个 Task<T>,那么返回将立即发生,因此,如果代码在 try/catch 块中,则不会捕获异常。类似地,如果代码在 using块中,它将立即释放对象。

```C#

Task DoSomethingAsync() {     using (var foo = new Foo())     {         //会立即返回,那么foo会被Dispose掉,所以会抛出异常         return foo.DoAnotherThingAsync();     } }

//可以正常工作

async Task DoSomethingAsync() {     using (var foo = new Foo())     {         return await foo.DoAnotherThingAsync();     } }

```

避免使用 .Wait() 或者 .Result,使用.GetAwaiter().GetResult() 代替

使用.Wait()或者.Result有在 GUI 应用程序中发生死锁

默认中,当一个未完成的 Taskawait的时候,当前的上下文将会在该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)` 时,代码将不再尝试恢复到以前的位置。这稍微提高了性能,并有助于避免死锁。

task.configureawait(true)时,如果可能,代码将在完成任务的线程中完成,从而避免了上下文切换。

使用 Task.Delay,而不使用Thread.Sleep使任务等待一段时间后执行。

使用Task.WaitAny 等待任意任务完成,使用 Task.WaitAll 等待所有任务完成。