禁用捕获所有库代码中的上下文,ConfigureAwait(false)

时间:2015-08-28 10:48:53

标签: c# .net async-await c#-5.0

使用await时,默认情况下会捕获SynchronizationContext(如果存在),并使用该上下文执行await(继续块)之后的代码块(这导致线程上下文切换)。

public async Task DoSomethingAsync()
{
     // We are on a thread that has a SynchronizationContext here.

     await DoSomethingElseAsync();

     // We are back on the same thread as before here 
     //(well sometimes, depending on how the captured SynchronizationContext is implemented)
}

虽然这个默认值可能在您希望在异步操作完成后返回UI线程的UI上下文中有意义,但它似乎没有意义作为大多数其他方案的默认值。对于内部库代码来说当然没有意义,因为

  1. 它带来了不必要的线程上下文切换的开销。
  2. 很容易意外产生死锁(记录为herehere)。
  3. 在我看来,微软已经决定了错误的默认设置。

    现在我的问题:

    是否还有其他(最好是更好的)解决方法,而不是使用await将代码中的所有.ConfigureAwait(false)调用混乱?这很容易忘记,使代码可读性降低。

    更新 在每种方法的开头调用await Task.Yield().ConfigureAwait(false);是否足够?如果这可以保证我将在没有SynchronizationContext的情况下进行线程,则所有后续的await调用都不会捕获任何上下文。

2 个答案:

答案 0 :(得分:2)

首先,await Task.Yield().ConfigureAwait(false)无法工作,因为Yield没有返回Task。还有其他方法可以跳到池线程,但也不推荐使用它们,请检查"Why was “SwitchTo” removed from Async CTP / Release?"

如果您仍然想要这样做,那么如果原始线程上存在同步上下文,那么利用ConfigureAwait(false) pushes the continuation to a pool thread的事实就是一个很好的技巧,即使有这里没有异步:

static Task SwitchAsync()
{
    if (SynchronizationContext.Current == null)
        return Task.FromResult(false); // optimize

    var tcs = new TaskCompletionSource<bool>();
    Func<Task> yield = async () =>
        await tcs.Task.ConfigureAwait(false);
    var task = yield();
    tcs.SetResult(false);
    return task;
}

// ...

public async Task DoSomethingAsync()
{
    // We are on a thread that has a SynchronizationContext here.
    await SwitchAsync().ConfigureAwait(false); 

    // We're on a thread pool thread without a SynchronizationContext 
    await DoSomethingElseAsync(); // no need for ConfigureAwait(false) here
    // ...       
}

同样,这是 not 我自己广泛使用的东西。关于使用ConfigureAwait(false)我已经similar concerns了。一个结果是,虽然ConfigureAwait(false)可能不是普遍完美的,但只要您不关心同步上下文就将其与await一起使用是可行的方法。这是.NET源代码本身紧随其后的准则。

另一个结果是,如果您担心DoSomethingElseAsync内可能未正确使用ConfigureAwait(false)的第三方代码,请执行以下操作:

public async Task DoSomethingAsync()
{
    // We are on a thread that has a SynchronizationContext here.
    await Task.Run(() => DoSomethingElseAsync()).ConfigureAwait(false);

    // We're on a thread pool thread without a SynchronizationContext 
    await DoYetSomethingElseAsync(); // no need for ConfigureAwait(false) here
    // ...       
}

这将使用接受Task.Run lambda的Func<Task>覆盖,在池线程上运行它并返回一个未包装的任务。您只能为await内的第一个DoSomethingAsync执行此操作。潜在成本与SwitchAsync相同:一个额外的线程切换,但代码更易读,结构更好。这是我在工作中使用的方法。

答案 1 :(得分:1)

  

有没有更好的方法来解决这个问题,而不是通过使用.ConfigureAwait(false)来混淆我的代码中的所有await调用?这很容易忘记并且使代码的可读性降低。

不是真的。没有任何“开箱即用”开关可以打开以改变该行为。有ConfigureAwaiter Checker ReSharper扩展可以提供帮助。另一种选择是滚动你自己的自定义扩展方法或SynchronizationContext包装器来切换上下文,或者甚至是自定义的awaiter。