【问题标题】:Unexpected behaviour after returning from await从等待返回后的意外行为
【发布时间】:2013-12-25 02:31:06
【问题描述】:

我知道有很多关于 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 方法,从它离开的地方继续。

它的作用。对我来说奇怪的是,为什么“主要”方法一直在运行?那是一根线从一根跳到另一根吗?还是有两个并行线程?

这不是线程安全问题吗?

我觉得这很令人困惑。我不是专家。谢谢。

【问题讨论】:

  • 我也建议阅读这个......msdn.microsoft.com/en-us/magazine/jj991977.aspx
  • 在控制台模式应用程序中没有任何机制可以继续在同一线程上运行。这需要一个同步提供程序,而您没有。一个 GUI 应用程序有一个,它“泵送消息循环”。您可以看到这不可能工作的最明显方法是查看您的主线程正在做什么。它在睡觉。睡着时无法执行代码。

标签: c# asynchronous async-await


【解决方案1】:

我有一个description on my blog of how async methods resume after an await。本质上,await 捕获当前的SynchronizationContext,除非它是null,在这种情况下它捕获当前的TaskScheduler。然后使用该“上下文”来安排方法的其余部分。

由于您正在执行控制台应用程序,因此没有SynchronizationContext,并且默认的TaskScheduler 被捕获以执行async 方法的其余部分。该上下文将async 方法排入线程池。除非您实际上给它一个主循环,其中包含一个排队到该主循环的SynchronizationContext(或TaskScheduler),否则不可能返回到控制台应用程序的主线程。

【讨论】:

  • 我已经添加了自己的答案。我想听听你的想法。
【解决方案2】:

阅读It's All About the SynchronizationContext,我相信它会变得不那么混乱。您所看到的行为非常合理。 Task.Delay 在内部使用 Win32 Kernel Timer API(即CreateTimerQueueTimer)。计时器回调在池线程上调用,与您的 Main 线程不同。这就是await 之后的其余Poetry 继续执行的地方。这是默认任务调度程序的工作方式,在启动 await 的原始线程上没有同步上下文的情况下。

因为您不执行 awaitPoetry() 任务(除非您返回 Task 而不是 void),因此其 for 循环继续与 while 并行执行循环进入你的Main。为什么,更重要的是,如何您希望它被“强制”回到Main 线程?要发生这种情况,必须有一些明确的同步点,线程不能简单地在while 循环的中间被中断。

在 UI 应用程序中,核心消息循环可以作为这种同步点。例如。对于 WinForms 应用程序,WindowsFormsSynchronizationContext 可以实现这一点。如果在主 UI 线程上调用 await Task.Delay(),则在 Application.Run 运行的消息循环的未来迭代中,await 之后的代码将在主 UI 线程上异步继续。

因此,如果它是一个 UI 线程,则 Poetry 的其余部分不会与 Poetry() 调用之后的 while 循环并行执行。相反,它将在控制流返回到消息循环时执行。或者,您可以使用Application.DoEvents() 显式发送消息以继续进行,尽管我不建议这样做。

附带说明,不要使用async void,而是使用async Taskmore info

【讨论】:

    【解决方案3】:

    当您调用异步例程时,这样做的目的是允许程序运行方法,同时仍允许调用例程、表单或应用程序继续响应用户输入(换句话说,继续正常执行)。 “await”关键字在使用它的点暂停执行,使用另一个线程运行任务,然后在线程完成时返回该行。

    因此,在您的情况下,如果您希望主例程暂停,直到“诗歌”例程完成,您需要使用 await 关键字,如下所示:

    void async Main()
    {
       await Poetry();
       while (true)
       {
           Console.WriteLine("Outside, within Main.");
          Thread.Sleep(200);
       }
    }
    

    您还需要更改 Poetry 的定义以允许使用 await 关键字:

    async Task Poetry()
    

    因为这个问题真的让我很感兴趣,所以我继续写了一个可以实际编译的示例程序。只需创建一个新的控制台应用程序并将此示例粘贴进去。您可以看到使用“等待”与不使用它的结果。

    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

    【讨论】:

    • -1 "使用另一个线程运行任务" 任务在单个线程上运行。您的代码无法编译您不能等待 void 方法
    • 尝试将“void”更改为 Task 我相信这就是它不允许等待的问题。我可以修改以显示。
    • 任务 不存在。要等待没有返回值的方法,您应该返回 Task
    • 是的,你是对的——希望现在答案应该是正确的。如果它不起作用,请告诉我。
    • 我添加了一个可以实际编译和运行的示例。我希望这个测试应用程序有助于真正了解“await”和“async”关键字的作用。
    【解决方案4】:

    我想在这里回答我自己的问题。

    你们中的一些人给了我很好的答案,所有这些都帮助我更好地理解(并且被赞了)。可能没有人给我一个完整的答案,因为我没有提出完整的问题。无论如何,有人会遇到我的确切误解,我希望这是第一个答案(但我会建议在下面查看更多答案)。

    因此,Task.Delay 使用Timer,它使用操作系统在 N 毫秒后触发事件。在这段时间之后创建了一个新的池线程,它基本上什么都不做。

    await 关键字意味着在线程完成后(它几乎什么都不做)它应该继续执行 await 关键字之后的任何内容。

    这里是同步上下文,如其他答案中所述。

    如果没有这样的上下文,相同的新创建的池线程将继续运行等待之后的任何内容。

    如果有同步上下文,新创建的池线程只会将等待之后的内容推送到同步上下文中。

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

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

    无论如何,我推荐阅读C# 5.0 In A Nutshell 的“并发与异步”一章。这对我帮助很大。很棒,而且实际上解释了整个故事。

    【讨论】:

    • 我从来不明白在没有评论的情况下大拇指向下的意义。我写的绝对是误导吗?我应该删除它吗?还不够相关吗?
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2013-09-29
    • 2023-04-04
    • 2013-05-13
    • 1970-01-01
    • 2021-12-16
    • 2019-06-28
    • 2018-09-14
    相关资源
    最近更新 更多