Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Does Task.Yield really run the continuation on the current context?

I'm no expert at async despite having written C# for many years, but AFAICT after reading some MSDN blog posts:

  • Awaitables (such as Task) may either capture or not capture the current SynchronizationContext.
  • A SynchronizationContext roughly corresponds to a thread: if I'm on the UI thread and call await task, which 'flows context', the continuation is run on the UI thread. If I call await task.ConfigureAwait(false), the continuation is run on some random threadpool thread which may/may not be the UI thread.
  • For awaiters: OnCompleted flows context, and UnsafeOnCompleted does not flow context.

OK, with that established, let's take a look at the code Roslyn generates for await Task.Yield(). This:

using System;
using System.Threading.Tasks;

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

Results in this compiler-generated code (you may verify yourself 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__);
    }
}

Notice that AwaitUnsafeOnCompleted is being called with the awaiter, instead of AwaitOnCompleted. AwaitUnsafeOnCompleted, in turn, calls UnsafeOnCompleted on the awaiter. YieldAwaiter does not flow the current context in UnsafeOnCompleted.

This really confuses me because this question seems to imply that Task.Yield does capture the current context; the asker is frustrated that at the lack of a version that doesn't. So I'm confused: does or doesn't Yield capture the current context?

If it doesn't, how can I force it to? I'm calling this method on the UI thread, and I really need the continuation to run on the UI thread, too. YieldAwaitable lacks a ConfigureAwait() method, so I can't write await Task.Yield().ConfigureAwait(true).

Thanks!

like image 780
James Ko Avatar asked Jul 13 '17 21:07

James Ko


People also ask

What is Task yield ()?

Remarks. You can use await Task. Yield(); in an asynchronous method to force the method to complete asynchronously. If there is a current synchronization context (SynchronizationContext object), this will post the remainder of the method's execution back to that context.

Is Task run asynchronous?

NET, Task. Run is used to asynchronously execute CPU-bound code.

What is the purpose of task run?

Remarks. The Run method allows you to create and execute a task in a single method call and is a simpler alternative to the StartNew method. It creates a task with the following default values: Its cancellation token is CancellationToken.

Does Task run need to be awaited?

If it is some trivial operation that executes quickly, then you can just call it synchronously, without the need for await . But if it is a long-running operation, you may need to find a way to make it asynchronous.


1 Answers

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

To summarize, ExecutionContext must always be flowed for developer code; to do otherwise is a security issue. There are certain scenarios (e.g., in compiler-generated code) where the compiler knows it's safe not to flow (i.e., it will be flowed by another mechanism). This is essentially what's happening in this scenario, as traced by Peter.

However, that doesn't have anything to do with the context captured by await (which is the current SynchronizationContext or TaskScheduler). Take a look at the logic in YieldAwaiter.QueueContinuation: if there is a current SynchronizationContext or TaskScheduler, it is always used and the flowContext parameter is ignored. This is because the flowContext parameter only refers to flowing the ExecutionContext and not the SynchronizationContext / TaskScheduler.

In contrast, the task awaiters end up at Task.SetContinuationForAwait, which has two bool parameters: continueOnCapturedContext for determining whether to capture the await context (SynchronizationContext or TaskScheduler), and flowExecutionContext for determining whether it's necessary to flow the ExecutionContext.

like image 99
Stephen Cleary Avatar answered Oct 08 '22 23:10

Stephen Cleary