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

bit*_*onk 7 .net c# 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)
}
Run Code Online (Sandbox Code Playgroud)

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

  1. 它带来了不必要的线程上下文切换的开销.
  2. 很容易意外地产生死锁(如此处或此处所述).

在我看来,微软已经决定了错误的默认值.

现在我的问题:

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

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

nos*_*tio 1

首先,await Task.Yield().ConfigureAwait(false)不会工作,因为Yield不返回Task. 还有其他方法可以跳转到池线程,但也不推荐使用它们,请检查“为什么 \xe2\x80\x9cSwitchTo\xe2\x80\x9d 从异步 CTP/发布中删除?”

\n\n

如果您仍然想这样做,这里有一个很好的技巧,利用这样一个事实:如果原始线程上有同步上下文,ConfigureAwait(false) 则将延续推送到池线程,即使这里没有异步:

\n\n
static Task SwitchAsync()\n{\n    if (SynchronizationContext.Current == null)\n        return Task.FromResult(false); // optimize\n\n    var tcs = new TaskCompletionSource<bool>();\n    Func<Task> yield = async () =>\n        await tcs.Task.ConfigureAwait(false);\n    var task = yield();\n    tcs.SetResult(false);\n    return task;\n}\n\n// ...\n\npublic async Task DoSomethingAsync()\n{\n    // We are on a thread that has a SynchronizationContext here.\n    await SwitchAsync().ConfigureAwait(false); \n\n    // We\'re on a thread pool thread without a SynchronizationContext \n    await DoSomethingElseAsync(); // no need for ConfigureAwait(false) here\n    // ...       \n}\n
Run Code Online (Sandbox Code Playgroud)\n\n

\n\n

再说一遍,这不是我自己会广泛使用的东西。我对使用也有类似的担忧ConfigureAwait(false)。一个结果是,虽然ConfigureAwait(false)可能不是普遍完美,但await只要您不关心同步上下文,就使用它是可行的方法。这是 .NET 源代码本身密切遵循的准则。

\n\n

另一个结果是,如果您担心其中的第三方代码DoSomethingElseAsync可能无法ConfigureAwait(false)正确使用,只需执行以下操作:

\n\n
public async Task DoSomethingAsync()\n{\n    // We are on a thread that has a SynchronizationContext here.\n    await Task.Run(() => DoSomethingElseAsync()).ConfigureAwait(false);\n\n    // We\'re on a thread pool thread without a SynchronizationContext \n    await DoYetSomethingElseAsync(); // no need for ConfigureAwait(false) here\n    // ...       \n}\n
Run Code Online (Sandbox Code Playgroud)\n\n

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

\n

  • 我相信您的“SwitchAsync()”有一个竞争条件:返回的“任务”可以在“等待”之前完成,在这种情况下,切换不会发生。您可以通过在“SetResult()”之后添加“Thread.Sleep(20);”(模拟长上下文切换)来观察这一点。 (2认同)