6

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!

James Ko
  • 32,215
  • 30
  • 128
  • 239
  • 1
    On a UI context, depending on what you're really trying to achieve, I'd suggest using `Dispatcher.Yield` instead: https://msdn.microsoft.com/en-us/library/system.windows.threading.dispatcher.yield(v=vs.110).aspx That said, I don't really understand your question. I mean, the answer to the first part of the question is trivial: create an UI application, call `await Task.Yield();`, check whether you are still on the UI thread, and voila – Kevin Gosse Jul 13 '17 at 22:01
  • @KevinGosse `Dispatcher.Yield()` isn't available to me, I'm writing a Xamarin.Android application. You guessed correctly at what I'm trying to achieve, though. I am trying to keep the UI responsive by calling `await Task.Yield()` at fixed intervals in the middle of CPU-intensive work on the UI thread. – James Ko Jul 13 '17 at 22:05
  • 3
    Solution: Do not process CPU-intensive work on the UI thread ;o) – Sir Rufo Jul 13 '17 at 22:08
  • @SirRufo, I have no choice. The CPU-intensive work happens to be code that modifies the UI, so I can't move it to a background thread. – James Ko Jul 13 '17 at 22:10
  • On WinRT, I had some issues with `Task.Yield` as it can resume execution before the UI actually has a chance to refresh. Fortunately, porting `Dispatcher.Yield` is possible: http://blog.wpdev.fr/dispatcher-yield-when-and-how-to-use-it-on-winrt/ – Kevin Gosse Jul 13 '17 at 22:13
  • 1
    Hmmm, setting a control property is not really cpu-intensive. You should ask on http://codereview.stackexchange.com how to refactor your code – Sir Rufo Jul 13 '17 at 22:16
  • @KevinGosse That's on WinRT unfortunately, I'm developing for Android :( Would be nice if someone knew how to do it for that platform. – James Ko Jul 13 '17 at 22:16
  • @SirRufo, It is for my case. I'm building an app that highlights source code, and for large files (3000+ lines) initializing the text editor can take quite a long time. I've profiled my app. – James Ko Jul 13 '17 at 22:18
  • You can modify the UI on a different thread than the UI. It takes a little more work, but that's no reason to keep long running processes on the GUI thread. – Michael Z. Jul 13 '17 at 22:19
  • @MichaelZ. Can you show me how (for Android)? I wasn't aware that was possible. – James Ko Jul 13 '17 at 22:20
  • I just noticed you are using xaramin so maybe there are limitations there, but it would be silly to leave out such functionality. I imagine there must be some way to create a delegate and then invoke that on the GUI thread to update the UI. – Michael Z. Jul 13 '17 at 22:25
  • @MichaelZ. Yes, there is a way to create a delegate and invoke it on the UI thread: `RunOnUiThread`. I've certainly tried that. However, it proved very hard to do so for this case and it required a hackish workaround that eventually fell apart. – James Ko Jul 13 '17 at 22:28
  • @JamesKo maybe it's the design that makes it complicated. Maybe start a question to ask about that or post it in code review. I think you should try and get the processor intensive task off the thread instead of trying to make your UI responsive. – Michael Z. Jul 13 '17 at 23:23
  • @JamesKo I'm not familiar with Xamarin, but it should be trivial to change the code I linked to call `RunOnUiThread` instead of `this.dispatcher.RunAsync`. Then it should work – Kevin Gosse Jul 14 '17 at 08:29
  • 1
    @JamesKo: `Task.Yield` may not keep your UI responsive. I'm not sure about Xamarin but I know it won't keep desktop Windows UIs responsive - you'd need `Task.Delay` with a non-zero delay. That said, a better solution is probably virtualization. – Stephen Cleary Jul 14 '17 at 14:19
  • @KevinGosse Thank you for your advice. I managed to create a custom awaitable that does that (actually, `new Handler(Looper.MainLooper).Post` since `RunOnUiThread` behaves differently on the UI thread, for anyone curious), and now the app is running more smoothly. – James Ko Jul 14 '17 at 14:20
  • @StephenCleary What do you mean by virtualization? – James Ko Jul 14 '17 at 14:22
  • @JamesKo: I mean data virtualization, so you only create UI elements on-demand as they are needed for display. – Stephen Cleary Jul 14 '17 at 14:55

2 Answers2

5

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.

Stephen Cleary
  • 437,863
  • 77
  • 675
  • 810
  • _"the flowContext parameter is ignored"_ -- thanks Stephen. I'd missed that nuance when I'd looked at the method earlier. IMHO, _this_ is the correct answer...I'm now not even sure why the continuation `Action`, created by `AsyncMethodBuilderCore `, captures the context. Maybe you can elaborate on that. – Peter Duniho Jul 14 '17 at 16:26
  • I also thought that was odd. I don't have an explanation of why it would behave that way - possibly due to how other methods use it? Or it might just be a leftover after refactoring? I really don't know. – Stephen Cleary Jul 14 '17 at 19:24
4

As noted in the comments, an easy way to answer your question is to just run the code and see what happens. You'll find that execution is resumed in the original context.

I think you have been distracted by a red herring. Yes, AwaitUnsafeOnCompleted() calls UnsafeOnCompleted(), which in turn passes false for the flowContext parameter to the QueueContinuation() method. But all this overlooks the fact that the AsyncMethodBuilderCore object used to create the continuation Action, creates that Action by capturing the context, so the continuation can be executed in the original context.

It doesn't matter what the AsyncVoidMethodBuilder used in the state machine does (at least, with respect to your question), because the continuation itself which is created handles getting back to the original context.

And indeed, this is a core feature of await. The API would be incredibly broken if by default, some await statements continued in the captured context, while others did not. A primary reason async/await is so powerful is that, not only does it allow writing code that uses asynchronous operations in a linear, synchronous-appearing fashion, it essentially eliminates all of the headaches we used to have trying to get back onto specific contexts (e.g. UI threads or ASP.NET contexts) on completion of some asynchronous operation.

Peter Duniho
  • 68,759
  • 7
  • 102
  • 136