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

本文关键字:上下文 ConfigureAwait false 代码 | 更新日期: 2023-09-27 18:10:09

当使用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. 很容易意外地产生死锁(如这里或这里所述)。

在我看来微软的默认设置是错误的。

现在我的问题是:

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

更新:在每个方法的开始调用await Task.Yield().ConfigureAwait(false);是否足够?如果这将保证我之后将在一个没有SynchronizationContext的线程上,那么所有随后的await调用将不会捕获任何上下文。

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

首先,await Task.Yield().ConfigureAwait(false)不能工作,因为Yield不返回Task。还有其他跳转到池线程的方法,但它们的使用也不推荐,检查"为什么"SwitchTo"从异步CTP/发布中删除?"

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

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
    // ...       
}

再次强调,这不是我自己会广泛使用的。我对使用ConfigureAwait(false)也有类似的担忧。一个结果是,虽然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重写,它接受Func<Task> lambda,在池线程上运行它并返回一个未包装的任务。您只会对DoSomethingAsync中的第一个await执行此操作。潜在的成本与SwitchAsync相同:一个额外的线程切换,但代码更具可读性和更好的结构化。这就是我在工作中使用的方法。

是否有更好的方法来解决这个问题,而不是在我的代码中与。configureawait (false)混淆所有等待调用?这很容易忘记,使代码可读性降低。

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