繁体   English   中英

ConfigureAwait 将延续推送到池线程

[英]ConfigureAwait pushes the continuation to a pool thread

这是一些 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继续推送到这里的池线程?

我用“推到池中的线程”在这里当主延续回调(的描述的情况下action参数TaskAwaiter.UnsafeOnCompleted被调用在一个线程,而二次回调(一传给ConfiguredTaskAwaiter.UnsafeOnCompleted )排队到一个池线程。

文档说:

continueOnCapturedContext ... true 尝试将延续编组回捕获的原始上下文; 否则为假。

我知道当前线程上安装了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 }

为什么 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);

您将看到您停留在主线程上(假设它是等待之前的当前上下文),因为在代码路径中没有实际执行的异步代码。 FooAsync(false).ConfigureAwait(false);相同的调用FooAsync(false).ConfigureAwait(false); 但是,会导致它在执行后跳转到线程池线程。

这是基于挖掘.NET Reference Source对这种行为的解释。

如果使用ConfigureAwait(true) ,则继续通过使用SynchronizationContextTaskScheduler TaskSchedulerAwaitTaskContinuation完成,在这种情况下一切都很清楚。

如果使用ConfigureAwait(false) (或者如果没有同步上下文要捕获),则通过AwaitTaskContinuation完成,它首先尝试内联延续任务,然后在无法内联时使用ThreadPool将其排队。

内联由IsValidLocationForInlining确定,它从不使用自定义同步上下文将任务内联到线程上。 然而,它最好将它内联到当前池线程上。 这解释了为什么我们在第一种情况下被推送到池线程上,而在第二种情况下(使用Task.Delay(100) )保持在同一个池线程上。

我认为最容易以稍微不同的方式思考这一点。

假设你有:

await task.ConfigureAwait(false);

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

否则, await将暂停该方法。 在这种情况下,当await恢复并看到ConfigureAwaitfalse ,有特殊的逻辑来检查代码是否具有SynchronizationContext ,如果是,则在线程池上恢复。 这是无证但并非不当行为。 因为它是无证的,我建议你不要依赖于行为; 如果你想在线程池上运行一些东西,使用Task.Run ConfigureAwait(false)字面意思是“我不在乎这个方法在什么上下文中恢复。”

请注意, ConfigureAwait(true) (默认值)将在当前SynchronizationContextTaskScheduler上继续该方法。 虽然ConfigureAwait(false)将在任何线程上继续该方法,除了具有SynchronizationContext线程。 它们并不完全相反。

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM