重访 Task.ConfigureAwait(continueOnCapturedContext: false)

Revisiting Task.ConfigureAwait(continueOnCapturedContext: false)

阅读时间太长。 使用 Task.ConfigureAwait(continueOnCapturedContext: false) 可能会引入冗余线程切换。我正在寻找一个一致的解决方案。

长版本。 ConfigureAwait(false) 背后的主要设计目标是尽可能减少 await 的冗余 SynchronizationContext.Post 继续回调。这通常意味着更少的线程切换和更少的 UI 线程工作。然而,它并不总是这样运作的。

例如,有一个第 3 方库实现了 SomeAsyncApi API。请注意,由于某些原因,ConfigureAwait(false) 未在该库中的任何地方使用:

// some library, SomeClass class
public static async Task<int> SomeAsyncApi()
{
    TaskExt.Log("X1");

    // await Task.Delay(1000) without ConfigureAwait(false);
    // WithCompletionLog only shows the actual Task.Delay completion thread
    // and doesn't change the awaiter behavior

    await Task.Delay(1000).WithCompletionLog(step: "X1.5");

    TaskExt.Log("X2");

    return 42;
}

// logging helpers
public static partial class TaskExt
{
    public static void Log(string step)
    {
        Debug.WriteLine(new { step, thread = Environment.CurrentManagedThreadId });
    }

    public static Task WithCompletionLog(this Task anteTask, string step)
    {
        return anteTask.ContinueWith(
            _ => Log(step),
            CancellationToken.None,
            TaskContinuationOptions.ExecuteSynchronously,
            TaskScheduler.Default);
    }
}

现在,假设在 WinForms UI 线程上有一些客户端代码 运行 并使用 SomeAsyncApi:

// another library, AnotherClass class
public static async Task MethodAsync()
{
    TaskExt.Log("B1");
    await SomeClass.SomeAsyncApi().ConfigureAwait(false);
    TaskExt.Log("B2");
}

// ... 
// a WinFroms app
private async void Form1_Load(object sender, EventArgs e)
{
    TaskExt.Log("A1");
    await AnotherClass.MethodAsync();
    TaskExt.Log("A2");
}

输出:

{ 步骤 = A1, 线程 = 9 }
{ 步骤 = B1, 线程 = 9 }
{ 步骤 = X1, 线程 = 9 }
{ 步骤 = X1.5,线程 = 11 }
{ 步骤 = X2, 线程 = 9 }
{ 步骤 = B2, 线程 = 11 }
{ 步骤 = A2,线程 = 9 }

这里,逻辑执行流程经过4次线程切换。 其中 2 个是多余的,由 SomeAsyncApi().ConfigureAwait(false) 引起。发生这种情况是因为 ConfigureAwait(false) pushes the continuation to ThreadPool 来自具有同步上下文的线程(在本例中为 UI 线程)。

在这种特殊情况下,MethodAsync 没有 ConfigureAwait(false) 会更好。然后它只需要 2 个线程切换 vs 4:

{ 步骤 = A1, 线程 = 9 }
{ 步骤 = B1, 线程 = 9 }
{ 步骤 = X1, 线程 = 9 }
{ 步骤 = X1.5,线程 = 11 }
{ 步骤 = X2, 线程 = 9 }
{ 步骤 = B2, 线程 = 9 }
{ 步骤 = A2,线程 = 9 }

然而,MethodAsync 的作者使用 ConfigureAwait(false) 是出于好意并遵循 the best practices,她对 SomeAsyncApi 的内部实现一无所知。 如果使用ConfigureAwait(false)"all the way"(也就是在SomeAsyncApi里面)也没有问题,但这不是她能控制的。

WindowsFormsSynchronizationContext(或DispatcherSynchronizationContext)就是这样,我们可能根本不关心额外的线程切换。然而,在 ASP.NET 中可能会发生类似的情况,其中 AspNetSynchronizationContext.Post 本质上是这样做的:

Task newTask = _lastScheduledTask.ContinueWith(_ => SafeWrapCallback(action));
_lastScheduledTask = newTask;

整个事情看起来像是一个人为的问题,但我确实看到了很多这样的生产代码,包括客户端和服务器端。我遇到的另一个有问题的模式:await TaskCompletionSource.Task.ConfigureAwait(false)SetResult 在与前者 await 捕获的同步上下文相同的同步上下文中被调用。同样,延续被冗余地推送到 ThreadPool。这种模式背后的原因是 "it helps to avoid deadlocks".

问题:鉴于 ConfigureAwait(false) 所描述的行为,我正在寻找一种使用 async/await 的优雅方式,同时仍然最小化冗余 thread/context 切换。理想情况下,可以使用现有的第 3 方库。

到目前为止我看过的内容:

如果有任何其他想法,我将不胜感激。

已更新,地址为

I would say that it's the other way around because as Stephen said in his answer "The purpose of ConfigureAwait(false) is not to induce a thread switch (if necessary), but rather to prevent too much code running on a particular special context." which you disagree with and is the root of your compliant.

由于您的回答已被编辑, 为了清楚起见,我不同意:

ConfigureAwait(false) goal is to reduce, as much as possible, the work the "special" (e.g. UI) threads need to process in spite of the thread switches it requires.

我也不同意你的观点current version of that statement. I'll refer you to the primary source, Stephen Toub's blog post:

Avoid Unnecessary Marshaling

If at all possible, make sure the async implementation you’re calling doesn’t need the blocked thread in order to complete the operation (that way, you can just use normal blocking mechanisms to wait synchronously for the asynchronous work to complete elsewhere). In the case of async/await, this typically means making sure that any awaits inside of the asynchronous implementation you’re calling are using ConfigureAwait(false) on all await points; this will prevent the await from trying to marshal back to the current SynchronizationContext. As a library implementer, it’s a best practice to always use ConfigureAwait(false) on all of your awaits, unless you have a specific reason not to; this is good not only to help avoid these kinds of deadlock problems, but also for performance, as it avoids unnecessary marshaling costs.

确实说目标是为了性能避免不必要的编组成本。线程切换(流过 ExecutionContext,除其他外) 一个很大的编组成本。

现在,它没有在任何地方说明目标是减少在 "special" 个线程或上下文上完成的工作量。

虽然这对于 UI 个线程可能有一定的意义,但我仍然不认为这是 ConfigureAwait 背后的 主要 目标。还有其他更结构化的方法可以最大程度地减少 UI 线程上的工作,例如使用 await Task.Run(work) 的块。

此外,最小化 AspNetSynchronizationContext 上的工作根本没有意义 - 它本身从一个线程流向另一个线程,这与 UI 线程不同。恰恰相反,一旦您处于 AspNetSynchronizationContext,您希望尽可能多地工作,以避免在处理 HTTP 请求的过程中进行不必要的切换。尽管如此,在 ASP.NET 中使用 ConfigureAwait(false) 仍然非常有意义:如果使用正确,它再次减少了服务器端线程切换。

The major design goal behind ConfigureAwait(false) is to reduce redundant SynchronizationContext.Post continuation callbacks for await, where possible. This usually means less thread switching and less work on the UI threads.

我不同意你的前提。 ConfigureAwait(false) 目标是尽可能减少需要整理回 "special"(例如 UI)上下文的工作 尽管 它可能需要关闭该上下文的线程切换。

如果目标是减少线程切换,您可以在所有工作中保持在相同的特殊上下文中,然后不需要其他线程。

要实现这一点,您应该在任何地方使用 ConfigureAwait ,您不关心执行延续的线程。如果你举个例子并适当地使用 ConfigureAwait 你只会得到一个开关(而不是没有它的 2 个):

private async void Button_Click(object sender, RoutedEventArgs e)
{
    TaskExt.Log("A1");
    await AnotherClass.MethodAsync().ConfigureAwait(false);
    TaskExt.Log("A2");
}

public class AnotherClass
{
    public static async Task MethodAsync()
    {
        TaskExt.Log("B1");
        await SomeClass.SomeAsyncApi().ConfigureAwait(false);
        TaskExt.Log("B2");
    }
}

public class SomeClass
{
    public static async Task<int> SomeAsyncApi()
    {
        TaskExt.Log("X1");
        await Task.Delay(1000).WithCompletionLog(step: "X1.5").ConfigureAwait(false);
        TaskExt.Log("X2");
        return 42;
    }
}

输出:

{ step = A1, thread = 9 }
{ step = B1, thread = 9 }
{ step = X1, thread = 9 }
{ step = X1.5, thread = 11 }
{ step = X2, thread = 11 }
{ step = B2, thread = 11 }
{ step = A2, thread = 11 }

现在,在您确实关心延续线程的地方(例如,当您使用 UI 控件时),您 "pay" 通过切换到该线程,将相关工作发布到该线程。您仍然从不需要该线程的所有工作中获益。

如果你想更进一步,从 UI 线程中删除这些 async 方法的同步工作,你只需要使用 Task.Run 一次,并添加另一个开关:

private async void Button_Click(object sender, RoutedEventArgs e)
{
    TaskExt.Log("A1");
    await Task.Run(() => AnotherClass.MethodAsync()).ConfigureAwait(false);
    TaskExt.Log("A2");
}

输出:

{ step = A1, thread = 9 }
{ step = B1, thread = 10 }
{ step = X1, thread = 10 }
{ step = X1.5, thread = 11 }
{ step = X2, thread = 11 }
{ step = B2, thread = 11 }
{ step = A2, thread = 11 }

此使用指南 ConfigureAwait(false) 是针对库开发人员的,因为这是它真正重要的地方,但重点是尽可能使用它,在这种情况下,您可以减少这些特殊上下文的工作,同时保持线程切换最少。


使用 WithNoContext 与在任何地方使用 ConfigureAwait(false) 的结果完全相同。然而,缺点是它与线程的 SynchronizationContext 混淆,并且您不知道 async 方法内部的这一点。 ConfigureAwait 直接影响当前 await 所以你有因果。

也使用 Task.Run,正如我所指出的,与在任何地方使用 ConfigureAwait(false) 具有完全相同的结果,并增加了卸载 async 方法的同步部分的附加值到 ThreadPool。如果需要这个,那么Task.Run就合适,否则ConfigureAwait(false)就够了。


现在,如果您在处理 ConfigureAwait(false) 未正确使用的错误库时,您可以通过删除 SynchronizationContext 来绕过它,但使用 Thread.Run 就可以了更简单、更清晰,将工作卸载到 ThreadPool 的开销可以忽略不计。

当您处理异步操作时,线程切换的开销太小而无需关心(一般而言)。 ConfigureAwait(false) 的目的不是引发线程切换(如有必要),而是防止在特定的特殊上下文中使用过多代码 运行。

The reasoning behind this pattern was that "it helps to avoid deadlocks".

然后堆栈潜水。

但我确实认为在一般情况下这不是问题。当我遇到没有正确使用 ConfigureAwait 的代码时,我只是将它包装在 Task.Run 中然后继续。线程切换的开销不值得担心。

显然,内置 ConfigureAwait(false) 的行为是在 ThreadPool 上调用 await 的延续。这样做的原因 I assume 是为了防止出现多个异步工作流正在等待同一个未完成的任务,然后在同一个线程上以序列化方式调用它们的延续。这种情况可能会导致死锁,以防一个工作流的继续被阻塞,并等待来自另一个工作流的信号。另一个工作流将永远没有机会发送信号,因为它的继续将位于同一个(阻塞的)线程的等待队列中。

如果您不希望这种情况发生在您的应用程序中(如果您确定一个任务永远不会被两个工作流等待),那么您可以尝试使用下面的自定义 ConfigureAwait2 方法:

public static ConfiguredTaskAwaitable2 ConfigureAwait2(this Task task,
    bool continueOnCapturedContext)
    => new ConfiguredTaskAwaitable2(task, continueOnCapturedContext);

public struct ConfiguredTaskAwaitable2 : INotifyCompletion
{
    private readonly Task _task;
    private readonly bool _continueOnCapturedContext;

    public ConfiguredTaskAwaitable2(Task task, bool continueOnCapturedContext)
    {
        _task = task; _continueOnCapturedContext = continueOnCapturedContext;
    }
    public ConfiguredTaskAwaitable2 GetAwaiter() => this;
    public bool IsCompleted { get { return _task.IsCompleted; } }
    public void GetResult() { _task.GetAwaiter().GetResult(); }
    public void OnCompleted(Action continuation)
    {
        var capturedContext = _continueOnCapturedContext ?
            SynchronizationContext.Current : null;
        _ = _task.ContinueWith(_ =>
        {
            if (capturedContext != null)
                capturedContext.Post(_ => continuation(), null);
            else
                continuation();
        }, default, TaskContinuationOptions.ExecuteSynchronously,
            TaskScheduler.Default);
    }
}

我在你的例子中用 .ConfigureAwait2(false) 替换了 .ConfigureAwait(false)(在方法 MethodAsync 内),我得到了这个输出:

{ step = A1, thread = 1 }
{ step = B1, thread = 1 }
{ step = X1, thread = 1 }
{ step = X1.5, thread = 4 }
{ step = X2, thread = 1 }
{ step = B2, thread = 1 }
{ step = A2, thread = 1 }