Reputation: 34519
I'm no expert at async despite having written C# for many years, but AFAICT after reading some MSDN blog posts:
Task
) may either capture or not capture the current SynchronizationContext
.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.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!
Upvotes: 6
Views: 1083
Reputation: 456517
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
.
Upvotes: 5
Reputation: 70671
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.
Upvotes: 4