【问题标题】:Does Task.Yield really run the continuation on the current context?Task.Yield 是否真的在当前上下文中运行延续?
【发布时间】:2017-07-14 22:46:20
【问题描述】:

尽管我写了很多年的 C#,但我并不是异步方面的专家,但是在阅读了一些 MSDN 博客文章后,我 AFAICT:

  • Awaitables(例如Task)可以捕获也可以不捕获当前的SynchronizationContext
  • SynchronizationContext 大致对应于一个线程:如果我在 UI 线程上并调用“流上下文”的await task,则继续在 UI 线程上运行。如果我调用await task.ConfigureAwait(false),则继续在一些可能/可能不是 UI 线程的随机线程池线程上运行。
  • 对于等待者: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__);
    }
}

请注意,AwaitUnsafeOnCompleted 是由 awaiter 调用的,而不是 AwaitOnCompletedAwaitUnsafeOnCompleted 反过来在等待者上调用 UnsafeOnCompletedYieldAwaiterdoes notUnsafeOnCompleted 中流动当前上下文。

这真的让我很困惑,因为this question 似乎暗示Task.Yield 确实捕获了当前上下文;提问者对缺少没有的版本感到沮丧。所以我很困惑:Yield 是否捕获当前上下文?

如果没有,我怎么能强迫它呢?我在 UI 线程上调用此方法,我也确实需要继续在 UI 线程上运行。 YieldAwaitable 缺少ConfigureAwait() 方法,所以我不能写await Task.Yield().ConfigureAwait(true)

谢谢!

【问题讨论】:

  • 在 UI 上下文中,取决于您真正想要实现的目标,我建议使用 Dispatcher.Yield 代替:msdn.microsoft.com/en-us/library/… 就是说,我不太明白您的问题。我的意思是,问题第一部分的答案很简单:创建一个 UI 应用程序,调用 await Task.Yield();,检查您是否仍在 UI 线程上,瞧
  • @KevinGosse Dispatcher.Yield() 对我不可用,我正在编写一个 Xamarin.Android 应用程序。不过,你猜对了我想要实现的目标。我试图通过在 UI 线程上 CPU 密集型工作的中间以固定间隔调用 await Task.Yield() 来保持 UI 响应。
  • 解决方案:不要在 UI 线程上处理 CPU 密集型工作;o)
  • 嗯,设置控件属性并不是真正的 CPU 密集型。你应该在codereview.stackexchange.com 上询问如何重构你的代码
  • @JamesKo:Task.Yield 可能无法让您的 UI 保持响应。我不确定 Xamarin,但我知道它不会让桌面 Windows UI 保持响应 - 你需要 Task.Delay 具有非零延迟。也就是说,更好的解决方案可能是虚拟化。

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


【解决方案1】:

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

总而言之,ExecutionContext必须始终流向开发人员代码;否则是一个安全问题。在某些情况下(例如,在编译器生成的代码中),编译器知道它是安全的 not 流动(即,它将通过另一种机制流动)。这基本上就是在这种情况下发生的事情,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 上下文(SynchronizationContextTaskScheduler),flowExecutionContext 用于确定是否有必要流ExecutionContext

【讨论】:

  • “flowContext 参数被忽略” -- 谢谢 Stephen。当我早先查看该方法时,我错过了这种细微差别。恕我直言,this 是正确答案...我现在甚至不确定为什么由AsyncMethodBuilderCore 创建的延续Action 捕获上下文。也许你可以详细说明一下。
  • 我也觉得这很奇怪。我没有解释为什么它会这样 - 可能是由于其他方法如何使用它?或者它可能只是重构后的剩余部分?我真的不知道。
【解决方案2】:

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

我认为你被红鲱鱼分心了。是的,AwaitUnsafeOnCompleted() 调用 UnsafeOnCompleted(),后者又将 falseflowContext 参数传递给 QueueContinuation() 方法。但是这一切都忽略了AsyncMethodBuilderCore对象用来创建延续Actioncreates that Action by capturing the context,所以延续可以在原来的上下文中执行。

状态机中使用的AsyncVoidMethodBuilder 的作用无关紧要(至少就您的问题而言),因为创建的延续本身处理返回到原始上下文。

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

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2019-03-15
    • 1970-01-01
    • 2019-04-26
    • 1970-01-01
    • 1970-01-01
    • 2015-02-11
    • 2023-03-16
    • 1970-01-01
    相关资源
    最近更新 更多