重温Task.ConfigureAwait(continueOnCapturedContext:false)

时间:2015-02-09 12:39:55

标签: c# .net task-parallel-library async-await

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

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

例如,有一个实施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");
}

输出:

{ 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(false) 引起。这是因为ConfigureAwait(false) pushes the continuation to ThreadPool来自具有同步上下文的线程(在本例中是UI线程)。

在这种特殊情况下,MethodAsync最好没有ConfigureAwait(false) 。然后它只需要2个线程开关与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(false)并且有the best practices的所有良好意图,并且她对SomeAsyncApi的内部实施一无所知。 如果ConfigureAwait(false)被使用&#34;一直&#34; (也就是SomeAsyncApi内部),这不会有问题,但是&#39她超出了她的控制范围。

它与WindowsFormsSynchronizationContext(或DispatcherSynchronizationContext)的关系如何,我们可能根本不关心额外的线程切换。但是,类似的情况可能发生在ASP.NET中,其中AspNetSynchronizationContext.Post实际上是这样做的:

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

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

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

到目前为止我看过的内容

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

    await Task.Run(() => SomeAsyncApi()).ConfigureAwait(false);
    
  • 另一个hackish解决方案可能是暂时从当前线程中删除同步上下文,因此它不会被内部调用链中的任何后续等待捕获(我之前提到它{{3} }):

    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将用于await延续。为了解释这一点,WithNoContext可能会像下面一样被改变,这会使这种黑客更具异国情调:

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

我很欣赏其他任何想法。

已更新,以解决here

  

我会说它是相反的,因为正如斯蒂芬所说的那样   他的回答&#34; ConfigureAwait(false)的目的不是诱导a   线程切换(如果需要),而是防止过多的代码   在特定的特殊环境中运行。&#34;你不同意和   是合规的根源。

由于你的答案已被编辑,@i3arnon's comment我不同意,为清楚起见:

  

ConfigureAwait(false)目标是尽可能减少工作量   &#34;特别&#34; (例如UI)线程需要尽管线程处理   它需要切换。

我也不同意你的here is your statement声明。我会将您推荐给主要来源,Stephen Toub的current version

  

避免不必要的封送

     

如果可能,请确保您正在调用的异步实现   不需要被阻塞的线程来完成操作   (这样,你可以使用普通的阻塞机制来等待   同步以便在其他地方完成异步工作)。在里面   async / await的情况,这通常意味着确保任何等待   在你正在调用的异步实现中使用   所有等待点上的ConfigureAwait(false);这将阻止等待   从尝试编组回到当前的SynchronizationContext。如   一个库实现者,这是一个永远使用的最佳实践   在所有等待中配置等待(假),除非你有   具体原因没有;这不仅有助于避免这些   各种各样的死锁问题,,但也有性能,因为它避免了   不必要的编组费用。

它的确表示目标是避免不必要的编组成本,以提高性能。线程切换(流动ExecutionContext,除其他外) 是一个很大的编组费用。

现在,它并没有说任何地方的目标是减少在特殊情况下完成的工作量。线程或上下文。

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

此外,最小化AspNetSynchronizationContext上的工作是没有意义的 - 它本身就是从线程流向线程,与UI线程不同。相反,一旦你AspNetSynchronizationContext,你想做尽可能多的工作,以避免在处理HTTP请求的过程中不必要的切换。尽管如此,在ASP.NET中使用ConfigureAwait(false)仍然是完全合理的:如果使用得当,它会再次减少服务器端的线程切换。

3 个答案:

答案 0 :(得分:16)

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

  

这种模式背后的原因是&#34;它有助于避免死锁&#34;。

叠加潜水。

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

答案 1 :(得分:7)

  

ConfigureAwait(false)背后的主要设计目标是尽可能减少用于等待的冗余SynchronizationContext.Post连续回调。这通常意味着更少的线程切换和更少的UI线程工作。

我不同意你的前提。 ConfigureAwait(false)目标是尽可能地减少需要编组的工作,特别是&#34; (例如,用户界面)上下文的线程切换可能需要关闭该上下文。

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

要实现这一点,您应该使用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控件时),您需要支付&#34;付费&#34;通过切换到该线程,通过将相关工作发布到该线程。你仍然从所有不需要该线程的工作中获益。

如果您想更进一步并从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的工作开销非常微不足道。

答案 2 :(得分:1)

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

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

ThreadPool

在您的示例中(在方法 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); } } 替换为 .ConfigureAwait(false),并且得到了以下输出:

.ConfigureAwait2(false)