从等待返回后出现意外行为

时间:2013-12-07 19:59:18

标签: c# asynchronous async-await

我知道有很多关于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”方法一直在运行?是一个线程从一个跳到另一个?还是有两个平行线程?

再一次不是线程安全问题吗?

我发现这令人困惑。我不是专家。感谢。

4 个答案:

答案 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线程上调用awaitApplication.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关键字之后的任何内容。

此处出现同步上下文,如其他答案中所述。

如果没有这样的上下文,那么新创建的同一个线程将继续运行等待之后的所有内容。

如果存在同步上下文,则新创建的池线程将仅将等待之后的任何内容推送到同步上下文中。

为此,这里有几点我没有意识到:

  • async / await之前没有做任何事情(技术上讲)。可能只是非常笨拙。
  • 这只是一些.NET 4.5类的语言支持。
  • 这很像收益率回报。它可能会将你的方法分解为几个方法,甚至可能会生成一个类,并使用BCL中的一些方法,但仅此而已。

无论如何,我建议阅读C# 5.0 In A Nutshell的章节“Concurrency and Asynchrony”。这对我帮助很大。它很棒,实际上解释了整个故事。