我有以下简单的控制台应用程序:
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;
}
}
输出
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
我知道控制台应用程序不提供SynchronizationContext,因此任务在线程池上运行。但令我惊讶的是,当从DoSomething
中的await恢复执行时,我们与await中的相同线程。我假设当我们恢复执行等待方法时,我们要么返回我们期待的线程,要么完全在另一个线程上。
有谁知道为什么?我的例子在某种程度上有缺陷吗?
答案 0 :(得分:4)
此行为是由于优化(这是一个实现细节)。
具体而言,await
计划的续约使用TaskContinuationOptions.ExecuteSynchronously
标志。这在任何地方都没有正式记录,但几个月前我确实遇到过此问题wrote it up on my blog。
Stephen Toub有一个blog post that is the best documentation on how ExecuteSynchronously
actually works。一个重要的一点是,如果该延续的任务调度程序与当前线程不兼容,ExecuteSynchronously
将不会实际同步执行。
正如您所指出的,控制台应用没有SynchronizationContext
,因此await
计划的任务延续将使用TaskScheduler.Current
(在本例中为TaskScheduler.Default
,线程池任务调度程序)。
当您通过Task.Run
启动另一个任务时,您将在线程池上显式执行它。因此,当它到达其方法的末尾时,它将完成其返回的任务,从而导致继续执行(同步)。由于await
捕获的任务调度程序是线程池调度程序(因此与延续兼容),因此它将直接执行DoSomething
的下一部分。
请注意,此处存在竞争条件。 DoSomething
的下一部分只有在已经附加作为Task.Run
返回的任务的延续时才会同步执行。在我的机器上,第一个Task.Run
将在另一个线程上恢复DoSomething
,因为Task.Run
代表完成时没有附加延续;第二个Task.Run
确实在同一个帖子上恢复了DoSomething
。
所以我修改了代码以使其更具确定性;这段代码:
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);
});
}
(在我的机器上)显示了竞争条件下的两种可能性:
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
顺便说一句,您对Task.Yield
的使用不正确;你必须await
结果才能真正做任何事情。
请注意,此行为(await
使用ExecuteSynchronously
)是未记录的实现细节,可能会在将来发生变化。
答案 1 :(得分:1)
当您没有指定使用哪个调度程序时,您可以随心所欲地决定运行任务的位置和方式。 await
真正做的就是将等待任务的所有代码放在等待任务完成之后运行的延续任务中。在许多情况下,调度程序会说“嘿,我刚刚在线程X上完成了一个任务,并且还有一个延续任务......自从线程X完成后,我将重新使用它来继续!”这正是您所看到的行为。 (有关详细信息,请参阅http://msdn.microsoft.com/en-US/library/vstudio/hh156528.aspx。)
如果您手动创建延续(而不是让await
为您这样做),您可以更好地控制延续的运行方式和位置。 (有关可以传递给Task.ContinueWith()
的延续选项,请参阅http://msdn.microsoft.com/en-us/library/system.threading.tasks.taskcontinuationoptions.aspx。)