Task.Yield是否真的在当前上下文中运行延续?

时间:2017-07-13 21:53:34

标签: c# .net asynchronous async-await task

尽管已经编写了多年的C#,但我没有async的专家,但是在阅读了一些MSDN博客文章后AFAICT:

  • 等待(例如Task)可以捕获或不捕获当前SynchronizationContext
  • 一个SynchronizationContext大致对应一个帖子:如果我在UI线程上并调用await task,其中'流动上下文',继续运行在UI线程。如果我调用await task.ConfigureAwait(false),则继续运行在某个随机线程池线程上,该线程可能是/可能不是UI线程。
  • 对于awaiters:OnCompleted流式上下文,而UnsafeOnCompleted不会传输上下文。

好了,有了这个,我们来看看Roslyn为await Task.Yield()生成的代码。这样:

using System;
using System.Threading.Tasks;

public class C {
    public async void M() {
        await Task.Yield();
    }
}

此编译器生成的代码中的结果(您可以自己验证here):

public class C
{
    [CompilerGenerated]
    [StructLayout(LayoutKind.Auto)]
    private struct <M>d__0 : IAsyncStateMachine
    {
        public int <>1__state;

        public AsyncVoidMethodBuilder <>t__builder;

        private YieldAwaitable.YieldAwaiter <>u__1;

        void IAsyncStateMachine.MoveNext()
        {
            int num = this.<>1__state;
            try
            {
                YieldAwaitable.YieldAwaiter yieldAwaiter;
                if (num != 0)
                {
                    yieldAwaiter = Task.Yield().GetAwaiter();
                    if (!yieldAwaiter.IsCompleted)
                    {
                        num = (this.<>1__state = 0);
                        this.<>u__1 = yieldAwaiter;
                        this.<>t__builder.AwaitUnsafeOnCompleted<YieldAwaitable.YieldAwaiter, C.<M>d__0>(ref yieldAwaiter, ref this);
                        return;
                    }
                }
                else
                {
                    yieldAwaiter = this.<>u__1;
                    this.<>u__1 = default(YieldAwaitable.YieldAwaiter);
                    num = (this.<>1__state = -1);
                }
                yieldAwaiter.GetResult();
                yieldAwaiter = default(YieldAwaitable.YieldAwaiter);
            }
            catch (Exception arg_6E_0)
            {
                Exception exception = arg_6E_0;
                this.<>1__state = -2;
                this.<>t__builder.SetException(exception);
                return;
            }
            this.<>1__state = -2;
            this.<>t__builder.SetResult();
        }

        [DebuggerHidden]
        void IAsyncStateMachine.SetStateMachine(IAsyncStateMachine stateMachine)
        {
            this.<>t__builder.SetStateMachine(stateMachine);
        }
    }

    [AsyncStateMachine(typeof(C.<M>d__0))]
    public void M()
    {
        C.<M>d__0 <M>d__;
        <M>d__.<>t__builder = AsyncVoidMethodBuilder.Create();
        <M>d__.<>1__state = -1;
        AsyncVoidMethodBuilder <>t__builder = <M>d__.<>t__builder;
        <>t__builder.Start<C.<M>d__0>(ref <M>d__);
    }
}

请注意,正在使用awaiter调用AwaitUnsafeOnCompleted,而不是AwaitOnCompleted。反过来,AwaitUnsafeOnCompleted会在等待者身上拨打UnsafeOnCompletedYieldAwaiter does notUnsafeOnCompleted中传输当前上下文。

这真让我感到困惑,因为this question似乎暗示Task.Yield 捕获当前的上下文;提问者对缺乏一个没有版本的版本感到沮丧。所以我很困惑:是或不是Yield捕获当前的背景?

如果它没有,我怎么强迫它?我在UI线程上调用此方法,我真的需要继续在UI线程上运行。 YieldAwaitable缺少ConfigureAwait()方法,因此我无法撰写await Task.Yield().ConfigureAwait(true)

谢谢!

2 个答案:

答案 0 :(得分:5)

如上所述in the comments,回答问题的一种简单方法就是运行代码并查看会发生什么。您会发现在原始上下文中恢复执行。

我认为你被红鲱鱼分心了。是的,AwaitUnsafeOnCompleted()调用了UnsafeOnCompleted()false会将flowContext参数的QueueContinuation()传递给AsyncMethodBuilderCore方法。但所有这些都忽略了Action对象用于创建延续AsyncVoidMethodBuildercreates that Action by capturing the context的事实,因此延续可以在原始上下文中执行。

状态机中使用的await是什么(至少就你的问题而言)并不重要,因为创建的continuation本身会处理回到原始上下文。

事实上,这是await的核心功能。如果默认情况下,某些async语句在捕获的上下文中继续,而其他语句则没有,那么API将会被打破。 await / [Any]如此强大的一个主要原因是,它不仅允许以线性,同步出现的方式编写使用异步操作的代码,它基本上消除了我们过去常常遇到的所有麻烦在完成某些异步操作时,我们试图回到特定的上下文(例如UI线程或ASP.NET上下文)。

答案 1 :(得分:4)

The ExecutionContext is not the same as the context captured by await (which is usually a SynchronizationContext)

总而言之,ExecutionContext 必须始终为开发人员代码流动;否则是一个安全问题。存在某些情况(例如,在编译器生成的代码中),其中编译器知道它是安全的而不是流(即,它将由另一机制流动)。这基本上就是在这种情况下发生的事情,as traced by Peter

但是,这与await(当前SynchronizationContextTaskScheduler)捕获的上下文无关。看看logic in YieldAwaiter.QueueContinuation:如果有当前SynchronizationContextTaskScheduler,则始终使用flowContext参数为忽略。这是因为flowContext参数仅指流ExecutionContext而不是SynchronizationContext / TaskScheduler

相比之下,the task awaiters end up at Task.SetContinuationForAwait有两个bool参数:continueOnCapturedContext,用于确定是否捕获await上下文(SynchronizationContext或{{1} })和TaskScheduler用于确定是否需要流动flowExecutionContext