ConfigureAwait 将 continuation 推送到池线程 [英] ConfigureAwait pushes the continuation to a pool thread

查看:11
本文介绍了ConfigureAwait 将 continuation 推送到池线程的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

这是一些 WinForms 代码:

async void Form1_Load(object sender, EventArgs e){//在 UI 线程上Debug.WriteLine(new { where = "before",Thread.CurrentThread.ManagedThreadId, Thread.CurrentThread.IsThreadPoolThread });var tcs = new TaskCompletionSource();this.BeginInvoke(new MethodInvoker(() => tcs.SetResult(true)));等待 tcs.Task.ContinueWith(t => {//仍然在 UI 线程上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 = 之前,ManagedThreadId = 10,IsThreadPoolThread = False }{ where = ContinueWith, ManagedThreadId = 10, IsThreadPoolThread = False }{ where = after, ManagedThreadId = 11, IsThreadPoolThread = True }

为什么 ConfigureAwait 会主动将 await 继续推送到这里的池线程?

我在这里使用推送到池线程"来描述主延续回调的情况(TaskAwaiter.UnsafeOnCompleted 已被调用线程,但辅助回调(传递给 ConfiguredTaskAwaiter.UnsafeOnCompleted) 排队到池线程.

文档 说:

<块引用>

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

我知道在当前线程上安装了 WinFormsSynchronizationContext.尽管如此,没有尝试编组,执行点已经存在.

因此,它更像是永远不要继续捕获的原始上下文"...

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

await Task.Delay(100).ContinueWith(t =>{//在池线程上Debug.WriteLine(new { where = "ContinueWith",Thread.CurrentThread.ManagedThreadId, Thread.CurrentThread.IsThreadPoolThread });}, TaskContinuationOptions.ExecuteSynchronously).ConfigureAwait(false);

<前>{ where = 之前,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();var thread = new Thread(() =>{Debug.WriteLine(new { where = "new Thread",Thread.CurrentThread.ManagedThreadId,Thread.CurrentThread.IsThreadPoolThread});SynchronizationContext.SetSynchronizationContext(new DumbSyncContext());tcs.SetResult(true);线程睡眠(1000);});线程.开始();等待 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 = 之前,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){如果 (!runSync)等待 Task.Delay(100);}

如果你这样称呼它:

await FooAsync(true).ConfigureAwait(false);

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

Here is some WinForms code:

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

The output:

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

Why does ConfigureAwait pro-actively push the await continuation to a pool thread here?

I use "pushing to a pool thread" here to describe the case when the primary continuation callback (the action parameter to TaskAwaiter.UnsafeOnCompleted has been invoked on one thread, but the secondary callback (the one passed to ConfiguredTaskAwaiter.UnsafeOnCompleted) is queued to a pool thread.

The docs say:

continueOnCapturedContext ... true to attempt to marshal the continuation back to the original context captured; otherwise, false.

I understand there's WinFormsSynchronizationContext installed on the current thread. Still, there is no attempt to marshal to be made, the execution point is already there.

Thus, it's more like "never continue on the original context captured"...

As expected, there's no thread switch if the execution point is already on a pool thread without a synchronization context:

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 }

Updated, one more test to see if any sync. context is not good enough for continuation (rather than the original one). This is indeed the case:

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 }

解决方案

Why ConfigureAwait pro-actively pushes the await continuation to a pool thread here?

It doesn't "push it to a thread pool thread" as much as say "don't force myself to come back to the previous SynchronizationContext".

If you don't capture the existing context, then the continuation which handles the code after that await will just run on a thread pool thread instead, since there is no context to marshal back into.

Now, this is subtly different than "push to a thread pool", since there isn't a guarantee that it will run on a thread pool when you do ConfigureAwait(false). If you call:

await FooAsync().ConfigureAwait(false);

It is possible that FooAsync() will execute synchronously, in which case, you will never leave the current context. In that case, ConfigureAwait(false) has no real effect, since the state machine created by the await feature will short circuit and just run directly.

If you want to see this in action, make an async method like so:

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

If you call this like:

await FooAsync(true).ConfigureAwait(false);

You'll see that you stay on the main thread (provided that was the current context prior to the await), since there is no actual async code executing in the code path. The same call with FooAsync(false).ConfigureAwait(false); will cause it to jump to thread pool thread after execution, however.

这篇关于ConfigureAwait 将 continuation 推送到池线程的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

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