问题
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?
回答1:
This behavior is due to an optimization (which is an implementation detail).
Specifically, the continuation scheduled by await
uses the TaskContinuationOptions.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 that ExecuteSynchronously
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 by await
will use TaskScheduler.Current
(which in this case is TaskScheduler.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 by await
was the thread pool scheduler (and therefore compatible with the continuation), it will just directly execute the next portion of DoSomething
.
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 by Task.Run
. On my machine, the first Task.Run
will resume DoSomething
on another thread because the continuation is not attached by the time the Task.Run
delegate completes; the second Task.Run
does resume DoSomething
on the same thread.
So I modified the code to be slightly more deterministic; this code:
static Task DoSomething()
{
return Task.Run(async () =>
{
Console.WriteLine("DoSomething-1 sleeping {0}", Thread.CurrentThread.ManagedThreadId);
await Task.Run(() =>
{
Console.WriteLine("DoSomething-1 sleep {0}", Thread.CurrentThread.ManagedThreadId);
Thread.Sleep(100);
});
Console.WriteLine("DoSomething-1 awake {0}", Thread.CurrentThread.ManagedThreadId);
Console.WriteLine("DoSomething-2 sleeping {0}", Thread.CurrentThread.ManagedThreadId);
var task = Task.Run(() =>
{
Console.WriteLine("DoSomething-2 sleep {0}", Thread.CurrentThread.ManagedThreadId);
});
Thread.Sleep(100);
await task;
Console.WriteLine("DoSomething-2 awake {0}", Thread.CurrentThread.ManagedThreadId);
});
}
(on my machine) shows both of the possibilities from the race condition:
Start 8
DoSomething-1 sleeping 9
DoSomething-1 sleep 10
DoSomething-1 awake 10
DoSomething-2 sleeping 10
DoSomething-2 sleep 11
DoSomething-2 awake 10
End 8
BTW, your use of Task.Yield
is incorrect; you have to await
the result to actually do anything.
Note that this behavior (await
using ExecuteSynchronously
) is an undocumented implementation detail and may change in the future.
回答2:
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 to Task.ContinueWith()
.)
来源:https://stackoverflow.com/questions/17559628/async-await-thread-transition-curiosity