ConfigureAwait将继续推送到池线程

时间:2014-03-26 21:02:40

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

这是一个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延续推送到池线程?

MSDN docs说:

  

continueOnCapturedContext ... true试图编组   继续回到原始上下文;否则,错误。

我理解当前线程上安装了WinFormsSynchronizationContext。仍然,没有尝试编组,执行点已经存在。

因此,它更像是&#34;永远不会继续捕获原始上下文&#34; ......

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

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 }

我即将查看implementation of ConfiguredTaskAwaitable的答案。

已更新,还有一项测试,看看是否任何同步。上下文不足以继续(而不是原始的)。确实如此:

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 个答案:

答案 0 :(得分:13)

  

为什么ConfigureAwait主动将await continuation推送到池线程?

它不会“将其推送到线程池线程”,就像说“不要强迫自己回到之前的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);的相同调用将导致它在执行后跳转到线程池线程。

答案 1 :(得分:10)

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

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

如果使用 ConfigureAwait(false) (或者如果没有要捕获的同步。上下文),则通过TaskSchedulerAwaitTaskContinuation完成,它会尝试内联续展首先执行任务,如果无法进行内联,则使用ThreadPool对其进行排队。

内联由AwaitTaskContinuation确定,它永远不会在具有自定义同步上下文的线程上内联任务。但是它最好在当前池线程上内联它。这就解释了为什么我们在第一种情况下推送池线程,并在第二种情况下保持在同一个池线程中(使用Task.Delay(100))。

答案 2 :(得分:8)

我认为以最略微不同的方式考虑这一点是最容易的。

假设你有:

await task.ConfigureAwait(false);

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

否则,await将暂停该方法。在这种情况下,当await恢复并看到ConfigureAwaitfalse时,有一个特殊的逻辑来检查代码是否有SynchronizationContext并在线程池上恢复就是这样。这是无证件的,但不是不正当的行为。因为它没有证件,我建议你不要依赖行为;如果要在线程池上运行某些内容,请使用Task.RunConfigureAwait(false)字面意思是“我不关心这种方法恢复的背景。”

请注意,ConfigureAwait(true)(默认设置)会继续使用当前SynchronizationContext TaskScheduler上的方法。虽然ConfigureAwait(false)将继续使用之外的任何线程上的方法SynchronizationContext。他们并不完全相反。