【问题标题】:disable capturing context in all library code, ConfigureAwait(false)在所有库代码中禁用捕获上下文,ConfigureAwait(false)
【发布时间】:2015-11-23 00:16:37
【问题描述】:

使用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)。

在我看来,微软已经决定使用错误的默认值。

现在我的问题:

除了将我的代码中的所有await 调用与.ConfigureAwait(false) 混淆之外,还有其他(最好是更好的)方法来解决这个问题吗?这很容易忘记,并且使代码的可读性降低。

更新: 在每个方法的开头调用await Task.Yield().ConfigureAwait(false); 可能就足够了吗?如果这可以保证我将在没有SynchronizationContext aferwards 的线程上,那么所有后续的await 调用都不会捕获任何上下文。

【问题讨论】:

  • 当然是一个非常丑陋的全局变量。揭示了添加 async/await 的真正原因,它为程序员提供了编写 WinRT 代码而不是死的机会。不是错误的默认值。作为一种处理异步性的通用机制,嗯,不是真的。任何线程抽象都会增加五个新的难以调试的问题。让它看起来太容易会很危险。
  • 您是否建议最好避免在库代码中使用 async/await?
  • 关于这个:// We are back on the same thread as before here. - 不一定在同一个线程上。它可能不是 ASP.NET 中的同一个线程,但仍然是同一个同步上下文。
  • @Noseratio 你说得对。
  • Task.Yield().ConfigureAwait(false) 无法编译。 Yield 不返回任务,它返回一个特制的等待对象。而且它不会让你脱离同步环境。

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


【解决方案1】:

首先,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
    // ...       
}

同样,这不是我自己会广泛使用的东西。我有similar concerns 关于使用ConfigureAwait(false)。一个结果是,虽然ConfigureAwait(false) 可能并不完美,但只要您不关心同步上下文,就可以将它与await 一起使用。这是 .NET 源代码本身严格遵循的准则。

另一个结果是,如果您担心 DoSomethingElseAsync 中的第 3 方代码可能无法正确使用 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
    // ...       
}

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

【讨论】:

  • 我相信你的SwitchAsync() 有一个竞争条件:返回的Task 可以在awaited 之前完成,在这种情况下,切换不会发生。您可以通过在SetResult() 之后添加Thread.Sleep(20);(模拟长上下文切换)来观察这一点。
  • 有趣的是,AwaitExtensions.SwitchTo 仍然存在于 Visual Studio SDK 中。
  • 关于这里不需要 ConfigureAwait(false)msdn.microsoft.com/en-us/magazine/jj991977.aspx 如果您可以在某个方法中的某个时间点使用 ConfigureAwait,那么我建议您将其用于在那之后,该方法中的每一次等待。回想一下,只有在等待不完整的任务时才会捕获上下文;如果任务已经完成,则不会捕获上下文。在不同的硬件和网络情况下,某些任务的完成速度可能比预期的要快,您需要优雅地处理在等待之前完成的返回任务
【解决方案2】:

有没有比使用 .ConfigureAwait(false) 将我的代码中的所有等待调用弄乱更好的方法来解决这个问题?这很容易忘记,并且降低了代码的可读性。

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

【讨论】:

  • 我也偶然发现了 ReSharper 检查器。但不幸的是,ReSharper 9 及更高版本不存在。
  • @bitbonk 我给作者发了邮件。也许他会把它移植到 9.2 SDK
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2017-06-06
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多