I have the following simple console application:
class Program
{
private static int times = 0;
static void Main(string[] args)
{
Console.WriteLine("Start {0}", Thread.CurrentThread.ManagedThreadId);
var task = DoSomething();
task.Wait();
Console.WriteLine("End {0}", Thread.CurrentThread.ManagedThreadId);
Console.ReadLine();
}
static async Task<bool> DoSomething()
{
times++;
if (times >= 3)
{
return true;
}
Console.WriteLine("DoSomething-1 sleeping {0}", Thread.CurrentThread.ManagedThreadId);
await Task.Run(() =>
{
Console.WriteLine("DoSomething-1 sleep {0}", Thread.CurrentThread.ManagedThreadId);
Task.Yield();
});
Console.WriteLine("DoSomething-1 awake {0}", Thread.CurrentThread.ManagedThreadId);
Console.WriteLine("DoSomething-2 sleeping {0}", Thread.CurrentThread.ManagedThreadId);
await Task.Run(() =>
{
Console.WriteLine("DoSomething-2 sleep {0}", Thread.CurrentThread.ManagedThreadId);
Task.Yield();
});
Console.WriteLine("DoSomething-2 awake {0}", Thread.CurrentThread.ManagedThreadId);
bool b = await DoSomething();
return b;
}
}
with the output
Start 1
DoSomething-1 sleeping 1
DoSomething-1 sleep 3
DoSomething-1 awake 4
DoSomething-2 sleeping 4
DoSomething-2 sleep 4
DoSomething-2 awake 4
DoSomething-1 sleeping 4
DoSomething-1 sleep 3
DoSomething-1 awake 3
DoSomething-2 sleeping 3
DoSomething-2 sleep 3
DoSomething-2 awake 3
End 1
I'm aware that console apps don't provide a SynchronizationContext so Tasks run on the thread pool. But what surprises me is that when resuming execution from an await in DoSomething
, we are on the same thread as we are on inside the await. I had assumed that we'd either return to the thread we awaited on or be on another thread entirely when we resume execution of the awaiting method.
Does anyone know why? Is my example flawed in some way?
When you're not specifying which scheduler to use, you're at the whim of "the system" to decide where/how to run your tasks. All that
await
really does is to place all of the code following the awaited task into a continuation task that runs after the awaited task completes. In many cases, the scheduler will say "hey, I just finished a task on thread X, and there's a continuation task as well... since thread X is done, I'll just re-use it for the continuation!" This is exactly the behavior you're seeing. (See http://msdn.microsoft.com/en-US/library/vstudio/hh156528.aspx for more details.)If you manually create your continuations (rather than letting
await
do so for you), you can have more control about exactly how and where the continuation runs. (See http://msdn.microsoft.com/en-us/library/system.threading.tasks.taskcontinuationoptions.aspx for the continuation options you can pass toTask.ContinueWith()
.)This behavior is due to an optimization (which is an implementation detail).
Specifically, the continuation scheduled by
await
uses theTaskContinuationOptions.ExecuteSynchronously
flag. This is not officially documented anywhere but I did encounter this a few months ago and wrote it up on my blog.Stephen Toub has a blog post that is the best documentation on how
ExecuteSynchronously
actually works. One important point is thatExecuteSynchronously
will not actually execute synchronously if the task scheduler for that continuation is not compatible with the current thread.As you pointed out, console apps don't have a
SynchronizationContext
, so task continuations scheduled byawait
will useTaskScheduler.Current
(which in this case isTaskScheduler.Default
, the thread pool task scheduler).When you start another task via
Task.Run
, you're explicitly executing it on the thread pool. So when it reaches the end of its method, it completes its returned task, causing the continuation to execute (synchronously). Since the task scheduler captured byawait
was the thread pool scheduler (and therefore compatible with the continuation), it will just directly execute the next portion ofDoSomething
.Note that there is a race condition here. The next portion of
DoSomething
will only execute synchronously if it is already attached as a continuation to the task returned byTask.Run
. On my machine, the firstTask.Run
will resumeDoSomething
on another thread because the continuation is not attached by the time theTask.Run
delegate completes; the secondTask.Run
does resumeDoSomething
on the same thread.So I modified the code to be slightly more deterministic; this code:
(on my machine) shows both of the possibilities from the race condition:
BTW, your use of
Task.Yield
is incorrect; you have toawait
the result to actually do anything.Note that this behavior (
await
usingExecuteSynchronously
) is an undocumented implementation detail and may change in the future.