我知道有很多关于async / await的问题,但我找不到任何答案。
我遇到了一些我不理解的东西,请考虑以下代码:
void Main()
{
Poetry();
while (true)
{
Console.WriteLine("Outside, within Main.");
Thread.Sleep(200);
}
}
async void Poetry()
{
//.. stuff happens before await
await Task.Delay(10);
for (int i = 0; i < 10; i++)
{
Console.WriteLine("Inside, after await.");
Thread.Sleep(200);
}
}
显然,在await运算符上,控件返回给调用者,而等待的方法正在后台运行。 (假设是IO操作)
但是在控制返回到await运算符之后,执行变为并行,而不是(我的期望)保持单线程。
我希望在“延迟”完成后,线程将被强制重新进入Poetry方法,从它离开的地方继续。
它做到了。对我来说奇怪的是,为什么“Main”方法一直在运行?是一个线程从一个跳到另一个?还是有两个平行线程?
再一次不是线程安全问题吗?
我发现这令人困惑。我不是专家。感谢。
答案 0 :(得分:8)
我有description on my blog of how async
methods resume after an await
。实质上,await
会捕获当前SynchronizationContext
,除非它是null
,在这种情况下它会捕获当前TaskScheduler
。然后使用该“上下文”来安排方法的其余部分。
由于您正在执行控制台应用,因此没有SynchronizationContext
,并且捕获默认的TaskScheduler
以执行async
方法的剩余部分。该上下文将async
方法排入队列。除非您实际为其提供一个主循环,其中SynchronizationContext
(或TaskScheduler
)排队到该主循环,否则无法返回到控制台应用程序的主线程。
答案 1 :(得分:3)
阅读It's All About the SynchronizationContext,我相信它会变得不那么混乱。你所看到的行为非常有意义。 Task.Delay
在内部使用Win32内核定时器API(即CreateTimerQueueTimer
)。在池线程上调用计时器回调,与您的Main
线程不同。这是Poetry
继续执行后await
的其余部分。这是默认任务调度程序的工作方式,因为在启动await
的原始线程上没有同步上下文。
由于您没有执行await
Poetry()
任务(除非您返回Task
而不是void
,否则不能)for
while
循环继续与Main
中的Main
循环并行执行。为什么,更重要的是,如何你希望它被“强制”回到while
线程?为了实现这一点,必须有一些明确的同步点,线程不能简单地在WindowsFormsSynchronizationContext
循环的中间被中断。
在UI应用程序中,核心消息循环可以用作这种类型的同步点。例如。对于WinForms应用程序,await Task.Delay()
会实现这一点。如果在主UI线程上调用await
,Application.Run
之后的代码将在主UI线程上异步继续,在Poetry
运行的消息循环的某个未来迭代中。
因此,如果它是一个UI线程,while
的其余部分将不会与Poetry()
调用之后的Application.DoEvents()
循环并行执行。相反,它将在控制流返回到消息循环时执行。或者,您可能会使用async void
显式地传递消息以继续发生,但我不建议这样做。
在旁注中,请勿使用async Task
,而应使用{{1}},more info。
答案 2 :(得分:1)
当您调用异步例程时,其目的是允许程序运行方法,同时仍允许调用例程,表单或应用程序继续响应用户输入(换句话说,继续正常执行)。 “await”关键字在使用它时暂停执行,使用另一个线程运行任务,然后在线程完成时返回到该行。
因此,在您的情况下,如果您希望主例程暂停直到“诗歌”例程完成,您需要使用这样的await关键字:
void async Main()
{
await Poetry();
while (true)
{
Console.WriteLine("Outside, within Main.");
Thread.Sleep(200);
}
}
您还需要更改Poetry的定义以允许使用await关键字:
async Task Poetry()
因为这个问题真的引起了我的兴趣,所以我继续写了一个你可以编译的示例程序。只需创建一个新的控制台应用程序并粘贴此示例。您可以看到使用“await”而不是使用它的结果。
class Program
{
static void Main(string[] args)
{
RunMain();
// pause long enough for all async routines to complete (10 minutes)
System.Threading.Thread.Sleep(10 * 60 * 1000);
}
private static async void RunMain()
{
// with await this will pause for poetry
await Poetry();
// without await this just runs
// Poetry();
for (int main = 0; main < 25; main++)
{
System.Threading.Thread.Sleep(10);
Console.WriteLine("MAIN [" + main + "]");
}
}
private static async Task Poetry()
{
await Task.Delay(10);
for (int i = 0; i < 10; i++)
{
Console.WriteLine("IN THE POETRY ROUTINE [" + i + "]");
System.Threading.Thread.Sleep(10);
}
}
}
快乐的测试!哦,你仍然可以阅读more information here。
答案 3 :(得分:0)
我想在这里回答我自己的问题。
你们中的一些人给了我很好的答案,这些答案都帮助我更好地理解(并且被大拇指了)。可能没有人给我一个完整的答案,因为我没有问完整的问题。在任何情况下,有人会遇到我的确切误解,我希望这是第一个答案(但我建议在下面再看一些答案)。
因此,Task.Delay使用Timer
,它使用操作系统在N毫秒后触发事件。在此期间之后创建了一个新的汇集线程,基本上什么也没做。
await关键字意味着在线程完成之后(并且它几乎什么也没做),它应该继续执行await关键字之后的任何内容。
此处出现同步上下文,如其他答案中所述。
如果没有这样的上下文,那么新创建的同一个线程将继续运行等待之后的所有内容。
如果存在同步上下文,则新创建的池线程将仅将等待之后的任何内容推送到同步上下文中。
为此,这里有几点我没有意识到:
无论如何,我建议阅读C# 5.0 In A Nutshell的章节“Concurrency and Asynchrony”。这对我帮助很大。它很棒,实际上解释了整个故事。