ConfigureAwait将后续操作推送到池线程上。

11

这是一些WinForms代码:

async void Form1_Load(object sender, EventArgs e)
{
    // on the UI thread
    Debug.WriteLine(new { where = "before", 
        Thread.CurrentThread.ManagedThreadId, Thread.CurrentThread.IsThreadPoolThread });

    var tcs = new TaskCompletionSource<bool>();

    this.BeginInvoke(new MethodInvoker(() => tcs.SetResult(true)));

    await tcs.Task.ContinueWith(t => { 
        // still on the UI thread
        Debug.WriteLine(new { where = "ContinueWith", 
            Thread.CurrentThread.ManagedThreadId, Thread.CurrentThread.IsThreadPoolThread });
    }, TaskContinuationOptions.ExecuteSynchronously).ConfigureAwait(false);

    // on a pool thread
    Debug.WriteLine(new { where = "after", 
        Thread.CurrentThread.ManagedThreadId, Thread.CurrentThread.IsThreadPoolThread });
}

输出结果:

{ where = before, ManagedThreadId = 10, IsThreadPoolThread = False }
{ where = ContinueWith, ManagedThreadId = 10, IsThreadPoolThread = False }
{ where = after, ManagedThreadId = 11, IsThreadPoolThread = True }

为什么 ConfigureAwait 在这里主动地将 await 继续执行推到池线程中呢?

这里使用"推送到池线程"来描述以下情况:当主继续回调(即传递给 TaskAwaiter.UnsafeOnCompletedaction参数)在一个线程上被调用时,而次要回调(传递给 ConfiguredTaskAwaiter.UnsafeOnCompleted 的回调函数)则排队到池线程。

文档中说:

continueOnCapturedContext...true表示尝试将继续返回到捕获的原始上下文;否则,为false。

我知道当前线程上安装了WinFormsSynchronizationContext。但是,这里没有尝试进行任何控制转移,执行点已经在那里。

因此,更像是 "从未在捕获的原始上下文上继续运行"...

如果执行点已经位于没有同步上下文的池线程上,则不会发生线程切换,正如预期的那样:

await Task.Delay(100).ContinueWith(t => 
{ 
    // on a pool thread
    Debug.WriteLine(new { where = "ContinueWith", 
        Thread.CurrentThread.ManagedThreadId, Thread.CurrentThread.IsThreadPoolThread });
}, TaskContinuationOptions.ExecuteSynchronously).ConfigureAwait(false);
{ where = before, ManagedThreadId = 10, IsThreadPoolThread = False }
{ where = ContinueWith, ManagedThreadId = 6, IsThreadPoolThread = True }
{ where = after, ManagedThreadId = 6, IsThreadPoolThread = True }

更新:再进行一次测试以查看是否任何同步上下文都不足以实现继续操作(而不是原始的同步上下文)。事实证明确实如此:

class DumbSyncContext: SynchronizationContext
{
}

// ...

Debug.WriteLine(new { where = "before", 
    Thread.CurrentThread.ManagedThreadId, 
    Thread.CurrentThread.IsThreadPoolThread });

var tcs = new TaskCompletionSource<bool>();

var thread = new Thread(() =>
{
    Debug.WriteLine(new { where = "new Thread",                 
        Thread.CurrentThread.ManagedThreadId,
        Thread.CurrentThread.IsThreadPoolThread});
    SynchronizationContext.SetSynchronizationContext(new DumbSyncContext());
    tcs.SetResult(true);
    Thread.Sleep(1000);
});
thread.Start();

await tcs.Task.ContinueWith(t => {
    Debug.WriteLine(new { where = "ContinueWith",
        Thread.CurrentThread.ManagedThreadId,
        Thread.CurrentThread.IsThreadPoolThread});
}, TaskContinuationOptions.ExecuteSynchronously).ConfigureAwait(false);

Debug.WriteLine(new { where = "after", 
    Thread.CurrentThread.ManagedThreadId, 
    Thread.CurrentThread.IsThreadPoolThread });
{ where = before, ManagedThreadId = 9, IsThreadPoolThread = False }
{ where = new Thread, ManagedThreadId = 10, IsThreadPoolThread = False }
{ where = ContinueWith, ManagedThreadId = 10, IsThreadPoolThread = False }
{ where = after, ManagedThreadId = 6, IsThreadPoolThread = True }

3个回答

18
为什么在这里使用ConfigureAwait会主动将await继续执行推送到线程池线程?实际上它并没有“将其推送到线程池线程”,而是说“不要强制我返回到之前的SynchronizationContext”。如果你不捕获现有的上下文,那么处理该await后面的代码的继续部分将在线程池线程上运行,因为没有上下文需要转发回去。现在,这与“推送到线程池”略有不同,因为使用ConfigureAwait(false)时不能保证它会在线程池上运行。如果你调用:
await FooAsync().ConfigureAwait(false);

如果 FooAsync() 同步执行,你将永远停留在当前上下文中。那时,ConfigureAwait(false) 将没有实际效果,因为由 await 语句创建的状态机将会绕过并直接运行。

如果你想看到这种情况的效果,请创建一个异步方法如下:

static Task FooAsync(bool runSync)
{
   if (!runSync)
       await Task.Delay(100);
}

如果您像这样调用它:

await FooAsync(true).ConfigureAwait(false);

如果在await之前的上下文是主线程,那么您会发现您仍然留在主线程中,因为代码路径中没有实际的异步代码正在执行。但是,使用 FooAsync(false).ConfigureAwait(false);进行相同的调用将导致它在执行后跳转到线程池线程。


你认为在最后一个示例中重用线程池线程只是运气好,还是也存在某种优化? - Scott Chamberlain
我实际上尝试过await Task.FromResult(true).ConfigureAwait(false) ,同步行为是有道理的。但我不明白为什么一个具有某些上下文的现有线程对于普通的连续操作来说不够好... - noseratio - open to work
@ScottChamberlain 这只是运气而已 - 它将在“某个”线程池线程上。 - Reed Copsey
@Noseratio 任何SynchronizationContext都足以回跨到 - 这不仅仅是“线程”的问题,而更多地是同步上下文的问题。例如,在ASP.Net应用程序中,它将是相同的同步上下文,但是一个不同的线程(可能)[当不使用ConfigureAwait] - Reed Copsey
@ReedCopsey,如果我使用ConfigureAwait(true),那么任何上下文都足以进行调用,这一点是理解的。但相反地,如果我使用ConfigureAwait(false),并且恰巧在恢复时位于任何上下文中,则会将该调用推送到线程池,而不是继续在当前上下文中执行,我的疑问是我试图澄清的规则。 - noseratio - open to work
@Noseratio,你需要停止以“线程”的方式思考——如果有消息泵,则SynchronizationContext可能是一个线程,否则不是。线程池线程没有同步上下文,因此没有什么可以捕获的。 - Reed Copsey

13

根据对.NET Reference Source的深入挖掘,这里解释了这种行为。

如果使用ConfigureAwait(true),则通过TaskSchedulerAwaitTaskContinuation执行继续,它使用SynchronizationContextTaskScheduler,这种情况很清楚。

如果使用ConfigureAwait(false)(或者没有同步上下文可以捕获),则它将通过AwaitTaskContinuation执行,该类首先尝试内联继续任务,然后如果无法内联,则使用ThreadPool进行排队。

内联由IsValidLocationForInlining决定,它永远不会在具有自定义同步上下文的线程上内联任务。但是,它会尽最大努力在当前池线程上内联它。这就解释了为什么我们在第一个案例中被推到池线程,并在第二个案例中保留在相同的池线程上(使用Task.Delay(100))。


2
好的回答。对我来说(也许本应如此),这并不是显而易见的,所以让我详细说明一下为什么我们不能仅仅内联,以及什么是“有效”的位置。“AwaitTaskContinuation.Run”中读到的一个注释表明了一个解释。事实证明,任何不在默认同步上下文中的线程都被“隐式地假定”具有可能的可用性要求。给出的示例是在线程池中运行的UI线程,它只能运行短暂的操作。由于没有办法知道,因此这个启发式方法没有冒任何风险。 - tne

12

我认为可以用稍微不同的方式来考虑这个问题。

假设你有:

await task.ConfigureAwait(false);

首先,如果任务已经完成,那么像Reed指出的那样,ConfigureAwait实际上会被忽略,执行会继续(同步地,在同一线程上)。

否则,await将暂停方法。在这种情况下,当await恢复并看到ConfigureAwaitfalse时,会有特殊逻辑检查代码是否具有SynchronizationContext,如果是,则在线程池上恢复执行。这是未记录但不是不当行为。因为它是未记录的,我建议您不要依赖这种行为;如果您想在线程池上运行某些东西,请使用Task.RunConfigureAwait(false)字面意思是“我不关心此方法在哪个上下文中恢复执行。”

请注意,ConfigureAwait(true)(默认值)将在当前的SynchronizationContextTaskScheduler上继续该方法。而ConfigureAwait(false)将在任何一个没有SynchronizationContext的线程上继续该方法。它们不完全是相反的。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接