ConfigureAwait的使用

Task.ConfigureAwait(bool)指示了Awaiter完成任务时是否尝试将结果返回原上下文中继续执行,Ture为尝试返回,False则相反。

在讨论ConfigureAwait的正确使用场景之前,则应该先了解SynchronizationContext类型。


SynchronizationContext

SynchronizationContext提供了在同步模型中传播上下文的基本功能,其中Send()方法将一个同步消息调度到一个上下文中,Post()方法将一个异步委托调度到一个上下文中。

例如,WinForms具有一个SynchronizationContext的派生类型,它重写了Post方法,以完成相当于Control.BeginInvoke的功能;这意味着对它的Post方法的任何调用都将导致委托稍后在该Control的UI线程上被调用。因此,想要在WinForms 的UI线程上正确执行一个委托方法,只需获得该UI线程的SynchronizationContext实例,将其作为Post的参数即可。

WPF也是如此,它有自己的SynchronizationContext派生类型,它重写了Post方法,同样可以将一个委托调度到UI线程上(通过Dispatcher.BeginInvoke)。

待补充例子

ConfigureAwait

  • 默认情况下,Task.ConfigureAwait(true)即默认调度回原上下文,排队回调可能涉及额外的分配工作,即更多的资源损耗。
  • 什么情况适合使用Task.ConfigureAwait(false)
    • 在不需要回到原始上下文中调用回调的时候可以使用;但类似WPF中需要在原有的上下文中(UI线程)修改界面的情况下,不应使用
    • 更高的性能,避免了不必要的队列,避免了不必要的线程访问

一般来说,编写通用库时可以使用Task.ConfigureAwait(false),因为通用库一般不在乎使用环境,即他不会做任何需要以特定方式与应用模型发生交互的事情。而在编写应用程序时,则更多的使用默认的Task.ConfigureAwait(true),比如WinForms、WPF、ASP.NET Core等。


DeadLock

如下文例子所示,错误的使用同步上下文调度器将导致死锁问题发生,MaxConcurrencySynchronizationContext是一个只有一个线程可以使用的同步上下文调度器。我们调用了Method2并阻塞它等待操作完成,Method2中调用了Method1方法,在控制台打印了相关信息后,await Task.Delay将会捕获当前的SynchronizationContext,并在操作完成后将回调返回至SynchronizationContext中排队执行。由于并发量限制了1且我们的代码已经阻塞了这个线程,死锁情况发生。

感谢黑洞视界大佬分享的知识。

void Main()
{
    Task.Run(() =>
    {
        var sc = new MaxConcurrencySynchronizationContext(1);
        SynchronizationContext.SetSynchronizationContext(sc);
        sc.Send( _ => {  Method2(); }, null);
    });
}

async Task Method1()
{
    Console.WriteLine($"Start:{nameof(Method1)}");
    await Task.Delay(100);
    Console.WriteLine($"End:{nameof(Method1)}");
}

void Method2()
{
    Console.WriteLine($"Start:{nameof(Method2)}");
    Method1().GetAwaiter().GetResult();
    Console.WriteLine($"End:{nameof(Method2)}");
}

internal sealed class MaxConcurrencySynchronizationContext : SynchronizationContext
{
    private readonly SemaphoreSlim _semaphore;

    public MaxConcurrencySynchronizationContext(int maxConcurrencyLevel)
    {
        _semaphore = new SemaphoreSlim(maxConcurrencyLevel);
    }

    public override void Post(SendOrPostCallback d, object state)
    {
        _semaphore.WaitAsync().ContinueWith(delegate
        {
            try
            {
                d(state);
            }
            finally
            {
                _semaphore.Release();
            }
        }, default, TaskContinuationOptions.None, TaskScheduler.Default);
    }

    public override void Send(SendOrPostCallback d, object state)
    {
        _semaphore.Wait();
        try
        {
            d(state);
        }
        finally
        {
            _semaphore.Release();
        }
    }
}

参考ConfigureAwait FAQ | .Net Blog