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

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

问题描述

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

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

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

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.

例如,有一个第三方库实施 SomeAsyncApi API。需要注意的是 ConfigureAwait(假)是不是在任何地方使用这个库,因为某些原因:

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

现在,让我们说有一些客户端code在一个WinForms UI线程中运行,并使用 SomeAsyncApi

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

输出:


{ 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 }

下面,逻辑执行流经过4线程切换。 2都是多余的,所造成的 SomeAsyncApi()。ConfigureAwait(假) 。这是因为 ConfigureAwait(假)推动延续线程池 的$ C $从具有同步上下文线程(在这种情况下,UI线程)。

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).

在这种特殊情况下, MethodAsync 是最好没有 ConfigureAwait(假) 。然后,只需要2线程切换VS 4:

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 }

不过, MethodAsync 用途作者 ConfigureAwait(假)所有的良好愿望和下面的最佳实践的,她什么都不知道关于内部实施 SomeAsyncApi 这不会是一个问题,如果 ConfigureAwait(假)用一路(即在 SomeAsyncApi 太),但是这超出了她的控制。

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.

这是它与 WindowsFormsSynchronizationContext 如何进入(或 DispatcherSynchronizationContext ),我们可能不关心额外的线程切换在所有。然而,类似的情况可能会在ASP.NET中,其中<一发生href="http://referencesource.microsoft.com/#System.Web/Util/SynchronizationHelper.cs,f0184c54fac66559"相对=nofollow> AspNetSynchronizationContext.Post 基本上做到这一点:

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;

整个事情看起来是一个人为的问题,但我没有看到大量的生产code这样,无论是客户端和服务器端。另一个可疑的模式,我碰到:等待TaskCompletionSource.Task.ConfigureAwait(假)的setResult 被称为在同一个同步背景作为捕获前计谋。此外,延续了冗余被推到线程池。后面这种模式的理由是,它有助于避免死锁。

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".

问题:在所描述的行为的光 ConfigureAwait(假),我正在寻找使用的一种优雅的方式异步/计谋,同时还最大限度地减少冗余线程/上下文切换。理想的情况是,这东西会工作存在的第三方库。

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.

我已经看了,至今

  • 减负的异步拉姆达与 Task.Run 是不理想的,因为它引入了至少一个额外线程切换(尽管它可以节省许多其他的):

  • 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);

  • 另外一个hackish的解决方案可能是从当前线程暂时删除同步环境,所以它不会被调用的内链在任何后续等待着被捕获(我previously提到的这里):

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

    这工作,但我不喜欢它的线程的当前同步上下文篡改,虽然在很短的范围内的事实。此外,还有另一种含义在这里:在没有的SynchronizationContext 在当前线程上,周围 TaskScheduler.Current 将用于计谋延续。考虑到这一点, WithNoContext 也可能会被改变如下图所示,这将使该劈更奇特的:

    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();
    

  • 我想AP preciate任何其他的想法。

    I'd appreciate any other ideas.

    更新,以解决<一个href="http://stackoverflow.com/questions/28410046/revisiting-task-configureawaitcontinueoncapturedcontext-false#comment45160263_28410872">@i3arnon's评论:

    我会说,这是周围的其他方法,因为斯蒂芬在说   他的答案的ConfigureAwait(假)的目的不是以诱导   线程切换(如果需要的话),而是以prevent太多code   在一个特定的特殊的环境中运行。你不同意和   是你的合规的根源。

    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.

    随着你的答案进行了编辑,<一个href="http://stackoverflow.com/questions/28410046/revisiting-task-configureawaitcontinueoncapturedcontext-false#comment45155636_28410872">here是你的发言我不同意,因为净度:

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

    ConfigureAwait(假)的目标是减少,尽可能地,工作   特殊(例如UI)的线程需要尽管线程的处理   切换它要求。

    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.

    我也是不同意你的当前版本。我将把你的主要来源,斯蒂芬Toub的博客帖子

    I also disagree with your current version of that statement. I'll refer you to the primary source, Stephen Toub's blog post:

    避免不必要的编组

    如果可能的话,请确保异步执行你打电话   不需要被阻塞的线程,以完成操作   (这样,你可以使用正常的阻塞机制等   同步异步工作,在其他地方完成)。在里面   情况下的异步/计谋,这通常意味着确保任何等待着   异步执行的内部你调用使用   ConfigureAwait(假)在所有等待点;这将prevent的等待   从试图元帅回到当前的SynchronizationContext。如   一个库的实现,它总是使用最佳实践   ConfigureAwait(假)在所有的等待着,除非你有一个   没有具体的原因;这是件好事,不仅有助于避免这些   种死锁问题,而且对性能,因为它避免了   不必要的封送处理的成本。

    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.

    虽然这可能使某些有意义的UI线程,我仍然不认为它是的主要的背后 ConfigureAwait 目标。还有其他的 - 更有条理 - 的方式,以尽量减少对UI线程的工作,如使用的块等待Task.Run(工作)

    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).

    此外,它没有任何意义的,以尽量减少对 AspNetSynchronizationContext 工作 - 这本身从线程流向线程,不像UI线程。截然相反,的一旦你的 AspNetSynchronizationContext ,你想尽可能多的工作,尽可能的,以避免在处理中间不必要的开关HTTP请求。尽管如此,它仍然非常有意义使用 ConfigureAwait(假)在ASP.NET:如果正确使用,再次降低了服务器端的线程切换

    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.

    推荐答案

    当您处理异步操作,线程切换的开销是远远太小关心(一般来说)。的目的 ConfigureAwait(假)不诱发线程切换(如果需要的话),而是prevent太多code在特定的特殊运行上下文。

    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".

    和堆栈潜水。

    但我不认为这是在一般情况下,一个非问题。当我遇到code,不正确使用 ConfigureAwait ,我只是把它包在 Task.Run 并继续前进。线程切换的开销是不值得担心。

    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:假)的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

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