重温 Task.ConfigureAwait(continueOnCapturedContext: false) [英] Revisiting Task.ConfigureAwait(continueOnCapturedContext: false)

查看:28
本文介绍了重温 Task.ConfigureAwait(continueOnCapturedContext: false)的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

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

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

例如,有一个 3rd 方库实现了 SomeAsyncApi API.注意 ConfigureAwait(false) 没有在这个库中的任何地方使用,出于某种原因:

//一些库,SomeClass 类公共静态异步任务SomeAsyncApi(){TaskExt.Log("X1");//等待 Task.Delay(1000) 没有 ConfigureAwait(false);//WithCompletionLog 只显示实际的 Task.Delay 完成线程//并且不会改变等待者的行为await Task.Delay(1000).WithCompletionLog(step: "X1.5");TaskExt.Log("X2");返回 42;}//日志助手公共静态部分类 TaskExt{公共静态无效日志(字符串步骤){Debug.WriteLine(new { step, thread = Environment.CurrentManagedThreadId });}public static Task WithCompletionLog(这个Task anteTask, string step){返回 anteTask.ContinueWith(_ =>日志(步骤),CancellationToken.None,TaskContinuationOptions.ExecuteSynchronously,TaskScheduler.Default);}}

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

//另一个库,AnotherClass 类公共静态异步任务 MethodAsync(){TaskExt.Log("B1");await SomeClass.SomeAsyncApi().ConfigureAwait(false);TaskExt.Log("B2");}//...//一个 WinFroms 应用private async void Form1_Load(object sender, EventArgs e){TaskExt.Log("A1");等待另一个类.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) 将延续推送到 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) 是出于好意并遵循 最佳实践,她对SomeAsyncApi的内部实现一无所知.如果 ConfigureAwait(false) 被一路"使用(即也在 SomeAsyncApi 内部),那不会有问题,但那是超出她的控制范围.

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

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

整个事情可能看起来像一个人为的问题,但我确实看到了很多这样的生产代码,包括客户端和服务器端.我遇到的另一个有问题的模式:await TaskCompletionSource.Task.ConfigureAwait(false)SetResult 被调用的同步上下文与之前的 await<捕获的同步上下文相同/代码>.同样,延续被冗余地推送到 ThreadPool.这种模式背后的原因是它有助于避免死锁".

问题:根据 ConfigureAwait(false) 的描述行为,我正在寻找一种使用 async/await<的优雅方式/code> 同时仍然最大限度地减少冗余线程/上下文切换.理想情况下,可以使用现有的 3rd 方库.

到目前为止我所看到的:

  • 使用 Task.Run 卸载 async lambda 并不理想,因为它至少引入了一个额外的线程切换(尽管它可能会节省许多其他线程):

    await Task.Run(() => SomeAsyncApi()).ConfigureAwait(false);

  • 另一种骇人听闻的解决方案可能是暂时从当前线程中删除同步上下文,因此它不会被内部调用链中的任何后续等待捕获(我之前提到过它此处):

    异步任务 MethodAsync(){TaskExt.Log("B1");await TaskExt.WithNoContext(() => SomeAsyncApi()).ConfigureAwait(false);TaskExt.Log("B2");}

    <前>{ 步骤 = A1,线程 = 8 }{ 步骤 = B1,线程 = 8 }{ 步 = X1,线程 = 8 }{ 步 = X1.5,线程 = 10 }{ 步 = X2,线程 = 10 }{ 步骤 = B2,线程 = 10 }{ 步骤 = A2,线程 = 8 }

    public static TaskWithNoContext(Func>func){任务<TResult>任务;var sc = SynchronizationContext.Current;尝试{SynchronizationContext.SetSynchronizationContext(null);//不要在这里等待任务,所以 SC 会在之后立即恢复//执行点到达 func 中的第一个 await任务 = func();}最后{SynchronizationContext.SetSynchronizationContext(sc);}返回任务;}

    这行得通,但我不喜欢它篡改线程的当前同步上下文,尽管范围很短.此外,这里还有另一个含义:在当前线程上没有 SynchronizationContext 的情况下,环境 TaskScheduler.Current 将用于 await 延续.为了解决这个问题,WithNoContext 可能会像下面这样修改,这会让这个 hack 更加奇特:

    //task = func();var task2 = new Task>(() => func());task2.RunSynchronously(TaskScheduler.Default);task = task2.Unwrap();

我很感激任何其他想法.

更新,以解决@i3arnon 的评论:

<块引用>

我会说这是相反的,因为正如斯蒂芬所说的他的回答ConfigureAwait(false) 的目的不是为了诱导线程切换(如有必要),而是为了防止代码过多在特定的特殊上下文中运行."您不同意并且是您合规的根源.

由于您的答案已被编辑,这是您的陈述我不同意,为清楚起见:

<块引用>

ConfigureAwait(false) 目标是尽可能地减少工作尽管有线程,但特殊"(例如 UI)线程需要处理切换它需要.

我也不同意您对该声明的当前版本.我会向您介绍主要来源,Stephen Toub 的 博客发帖:

<块引用>

避免不必要的编组

如果可能,请确保您正在调用的异步实现不需要阻塞的线程来完成操作(这样,您可以只使用正常的阻塞机制来等待同步以便异步工作在别处完成).在里面异步/等待的情况,这通常意味着确保任何等待您正在调用的异步实现内部正在使用在所有等待点上配置Await(false);这将防止等待从尝试封送回当前 SynchronizationContext.作为库实现者,始终使用是最佳实践ConfigureAwait(false) 在你所有的等待上,除非你有不这样做的具体原因;这不仅有助于避免这些各种死锁问题,但也是为了性能,因为它避免了不必要的编组成本.

它确实表示目标是避免为了性能而产生不必要的编组成本.线程切换(流过 ExecutionContext 等) 是一个很大的封送成本.

现在,它并没有说目标是减少在特殊"线程或上下文上完成的工作量.

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

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

解决方案

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

<块引用>

这种模式背后的原因是它有助于避免死锁".

和堆栈潜水.

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

Too long to read. Using Task.ConfigureAwait(continueOnCapturedContext: false) may be introducing redundant thread switching. I'm looking for a consistent solution to that.

Long version. 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. However, it isn't always how it works.

For example, there is a 3rd party library implementing SomeAsyncApi API. Note that ConfigureAwait(false) is not used anywhere in this library, for some reason:

// 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);
    }
}

Now, let's say there's some client code running on a WinForms UI thread and using 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");
}

The output:

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

Here, the logical execution flow goes through 4 thread switches. 2 of them are redundant and caused by SomeAsyncApi().ConfigureAwait(false). It happens because ConfigureAwait(false) pushes the continuation to ThreadPool from a thread with synchronization context (in this case, the UI thread).

In this particular case, MethodAsync is better off without ConfigureAwait(false). Then it only takes 2 thread switches vs 4:

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

However, the author of MethodAsync uses ConfigureAwait(false) with all good intentions and following the best practices, and she knows nothing about internal implementation of SomeAsyncApi. It wouldn't be a problem if ConfigureAwait(false) was used "all the way" (i.e., inside SomeAsyncApi too), but that's beyond her control.

That's how it goes with WindowsFormsSynchronizationContext (or DispatcherSynchronizationContext), where we might be not caring about extra thread switches at all. However, a similar situation could happen in ASP.NET, where AspNetSynchronizationContext.Post essentially does this:

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

The whole thing may look as a contrived issue, but I did see a lot of production code like this, both client-side and server-side. Another questionable pattern I came across: await TaskCompletionSource.Task.ConfigureAwait(false) with SetResult being called on the same synchronization context as that captured for the former await. Again, the continuation was redundantly pushed to ThreadPool. The reasoning behind this pattern was that "it helps to avoid deadlocks".

The question: In the light of the described behavior of ConfigureAwait(false), I'm looking for an elegant way of using async/await while still minimizing redundant thread/context switching. Ideally, something that would work existing 3rd party libraries.

What I've looked at, so far:

  • Offloading an async lambda with Task.Run is not ideal as it introduces at least one extra thread switch (although it can potentially save many others):

    await Task.Run(() => SomeAsyncApi()).ConfigureAwait(false);
    

  • One other hackish solution might be to temporarily remove synchronization context from the current thread, so it won't be captured by any subsequent awaits in the inner chain of calls (I previously mentioned it here):

    async Task MethodAsync()
    {
        TaskExt.Log("B1");
        await TaskExt.WithNoContext(() => SomeAsyncApi()).ConfigureAwait(false);
        TaskExt.Log("B2");
    }
    

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

    public static Task<TResult> WithNoContext<TResult>(Func<Task<TResult>> func)
    {
        Task<TResult> task;
        var sc = SynchronizationContext.Current;
        try
        {
            SynchronizationContext.SetSynchronizationContext(null);
            // do not await the task here, so the SC is restored right after
            // the execution point hits the first await inside func
            task = func();
        }
        finally
        {
            SynchronizationContext.SetSynchronizationContext(sc);
        }
        return task;
    }
    

    This works, but I don't like the fact that it tampers with the thread's current synchronization context, albeit for a very short scope. Moreover, there's another implication here: in the absence of SynchronizationContext on the current thread, an ambient TaskScheduler.Current will be used for await continuations. To account for this, WithNoContext could possibly be altered like below, which would make this hack even more exotic:

    // task = func();
    var task2 = new Task<Task<TResult>>(() => func());
    task2.RunSynchronously(TaskScheduler.Default); 
    task = task2.Unwrap();
    

I'd appreciate any other ideas.

Updated, to address @i3arnon's comment:

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.

As your answer has been edited, here is your statement I disagreed with, for clarity:

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.

I also disagree with your 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.

It does says that the goal is to avoid unnecessary marshaling costs, for performance. A thread switch (which flows the ExecutionContext, among other things) is a big marshaling cost.

Now, it doesn't say anywhere that the goal is to reduce the amount of work which is done on "special" threads or contexts.

While this may make certain sense for UI threads, I still don't think it is the major goal behind ConfigureAwait. There are other - more structured - ways to minimize work on UI threads, like using chunks of await Task.Run(work).

Moreover, it doesn't make sense at all to minimize work on AspNetSynchronizationContext - which itself flows from thread to thread, unlike with a UI thread. Quite opposite, once you're on AspNetSynchronizationContext, you want to make as much work as possible, to avoid unnecessary switching in the middle of handling the HTTP request. Nevertheless, it still makes perfect sense to use ConfigureAwait(false) in ASP.NET: if used correctly, it again reduces the server-side thread switching.

解决方案

When you're dealing with asynchronous operations, the overhead of a thread switch is way too small to care about (generally speaking). 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.

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

And stack dives.

But I do think this is a non-problem in the general case. When I encounter code that doesn't properly use ConfigureAwait, I just wrap it in a Task.Run and move on. The overhead of thread switches isn't worth worrying about.

这篇关于重温 Task.ConfigureAwait(continueOnCapturedContext: false)的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
相关文章
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆