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

2,048 阅读6分钟

用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

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

public async Task<string> 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<string> 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块中,它将立即释放对象。


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

//可以正常工作

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

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

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

默认中,当一个未完成的 Taskawait的时候,当前的上下文将会在该Task完成的时候重新获得并继续执行剩余的代码。这个context就是当前的SynchronizationContext ,除非它是空的。GUI应用程序的SynchronizationContext有排他性,只允许一个线程运行。

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中,这会增加错误处理的复杂性。


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() 将返回一个常规的异常


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可以提高性能 。


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后还有一些界面元素的操作,就会抛出线程异常了。      


private async void TextButton_OnClick(object sender, RoutedEventArgs e)
{
    TextButton.IsEnabled = false;
    try
    {
        await Task.Delay(2000).ConfigureAwait(false);
    }
    finally
    {
        TextButton.IsEnabled = true;//这里会抛出异常
    }
}

可以修改为:

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 等待所有任务完成。