为什么我的异步代码在调试时同步运行?

时间:2013-07-06 21:36:51

标签: c# visual-studio asynchronous async-await

我正在尝试使用异步功能实现一个名为ReadAllLinesAsync的方法。我已经生成了以下代码:

private static async Task<IEnumerable<string>> FileReadAllLinesAsync(string path)
{
    using (var reader = new StreamReader(path))
    {
        while ((await reader.ReadLineAsync()) != null)
        {

        }
    }
    return null;
}

private static void Main()
{
    Button buttonLoad = new Button { Text = "Load File" };
    buttonLoad.Click += async delegate
    {
        await FileReadAllLinesAsync("test.txt"); //100mb file!
        MessageBox.Show("Complete!");
    };

    Form mainForm = new Form();
    mainForm.Controls.Add(buttonLoad);
    Application.Run(mainForm);
}

我希望列出的代码能够异步运行,事实上,确实如此!但只有当我运行代码而没有 Visual Studio调试器时。

当我运行代码 附加的Visual Studio Debugger时,代码会同步运行,阻塞主线程导致UI挂起。

我尝试并成功地在三台机器上重现了这个问题。每个测试都是使用Visual Studio 2012在64位计算机(Windows 8或Windows 7)上进行的。

我想知道为什么会出现这个问题以及如何解决它(因为没有调试器的运行可能会阻碍开发)。

4 个答案:

答案 0 :(得分:11)

问题是你在一个没有做任何事情的紧密循环中调用await reader.ReadLineAsync() - 除了在每次等待之后返回执行UI线程之后再重新开始。只有在ReadLineAsync()尝试读取一行时,您的UI线程才能自由处理Windows事件。

要解决此问题,您可以将通话更改为await reader.ReadLineAsync().ConfigureAwait(false)

await等待异步调用的完成,并将执行返回到首先调用await的Syncrhonization上下文。在桌面应用程序中,这是UI线程。这是一件好事,因为它允许您直接更新UI,但如果您在await之后立即处理异步调用的结果,则会导致阻塞。

您可以通过指定ConfigureAwait(false)来更改此行为,在这种情况下,在不同的线程中继续执行,而不是原始的同步上下文。

即使它不仅仅是一个紧密的循环,你的原始代码也会阻塞,因为处理数据的循环中的任何代码仍将在UI线程中执行。要在不添加ConfigureAwait的情况下异步处理数据,您应该使用eg创建的数据处理数据。 Task.Factory.StartNew并等待该任务。

以下代码不会阻止,因为处理是在不同的线程中完成的,允许UI线程处理事件:

while ((line= await reader.ReadLineAsync()) != null)
{
    await Task.Factory.StartNew(ln =>
    {
        var lower = (ln as string).ToLowerInvariant();
        Console.WriteLine(lower);
     },line);
}

答案 1 :(得分:8)

我在某种程度上看到了和你一样的问题 - 但只是在一定程度上。对我来说,UI在调试器中非常不稳定,并且偶尔不会在调试器中生涩。 (顺便说一下,我的文件包含许多10个字符的行 - 数据的形状将在这里改变行为。)通常在调试器中开始时很好,然后很长时间不好,它有时会恢复。

怀疑问题可能只是因为你的磁盘太快而你的线太短了。我知道这听起来很疯狂,所以让我解释一下......

当您使用await表达式时,如果需要,将通过“附加连续”路径。如果结果已经存在,则代码只提取值并继续在同一个线程中。

这意味着,如果ReadLineAsync总是返回一个在返回时完成的任务,那么您将有效地看到同步行为。完全有可能ReadLineAsync查看它已被缓冲的数据,并尝试在其中同步查找一行开头。然后,操作系统可能会从磁盘读取更多数据,以便您的应用程序可以使用...这意味着UI线程永远不会有机会抽取其正常消息,因此UI会冻结。

期望通过网络运行相同的代码会“修复”问题,但似乎没有。 (请注意,它会精确地改变抽搐的显示方式。)但是,使用:

await Task.Delay(1);

解冻UI。 (Task.Yield不会,这再次让我感到困惑。我怀疑这可能是延续和其他UI事件之间的优先级问题。)

现在,为什么你只是在调试器中看到这个 - 这仍然让我感到困惑。也许这与调试器中如何处理中断有关,可以巧妙地改变时序。

这些只是猜测,但它们至少是有些受过教育的。

编辑:好的,我已经找到了一种方法来表明它至少部分与之相关。像这样改变你的方法:

private static async Task<IEnumerable<string>>
    FileReadAllLinesAsync(string path, Label label)
{
    int completeCount = 0;
    int incompleteCount = 0;
    using (var reader = new StreamReader(path))
    {
        while (true)
        {
            var task = reader.ReadLineAsync();
            if (task.IsCompleted)
            {
                completeCount++;
            }
            else
            {
                incompleteCount++;
            }
            if (await task == null)
            {
                break;
            }
            label.Text = string.Format("{0} / {1}",
                                       completeCount,
                                       incompleteCount);
        }
    }
    return null;
}

...并为UI创建并添加合适的标签。在我的机器上,无论是在调试还是非调试中,我都看到更“完整”的命中而不是“不完整” - 奇怪的是,完整与不完整的比例始终是84:1,两者都在调试器而不是。所以只有在阅读了大约85行之后,UI 才能才有机会进行更新。您应该在您的机器上尝试相同的操作。

作为另一个测试,我在label.Paint事件中添加了一个递增计数器 - 在调试器中它只执行了调试器中没有多次的1/10,对于相同的行数。

答案 2 :(得分:2)

Visual Studio实际上并不是同步执行异步回调。但是,您的代码的结构是这样的,即它会在UI线程中“泛滥”您可能不需要在UI线程上执行的消息。具体来说,当FileReadAllLinesAsyncwhile循环的正文中继续执行时,它会在同一方法中SynchronizationContext行上捕获的await上执行此操作。这意味着对于文件中的每一行,都会将一条消息发回到UI线程,以执行该while循环体的1个副本。

您可以谨慎使用ConfigureAwait(false)来解决此问题。

  1. FileReadAllLinesAsync中,while循环的主体对其运行的线程不敏感,因此您可以使用以下代码:

    while ((await reader.ReadLineAsync().ConfigureAwait(false)) != null)
    
  2. Main中,假设您想要在UI线程上执行MessageBox.Show行(也许您还有一个buttonLoad.Enabled = true语句)。您可以(而且会!)仍然可以在不对Main进行任何更改的情况下获得此行为,因为您没有在那里使用ConfigureAwait(false)

  3. 我怀疑您在调试器中观察到的延迟是由于.NET在托管/非托管代码中连接调试器时性能较慢,因此将这些数百万条消息中的每条消息分发到UI线程时,速度会慢100倍调试器附加。我怀疑上面的第1项将立即解决您的大部分问题,而不是试图通过禁用功能加快调度速度。

答案 3 :(得分:1)

从Microsoft下载中心的Task-based Asynchronous Pattern

  

出于性能原因,如果任务已经完成了   等待任务,控制不会产生和功能   将改为继续执行。

  

在某些情况下,完成操作所需的工作量   小于启动操作所需的工作量   异步(例如,从可以读取的流中读取)   对已经缓冲在内存中的数据感到满意。在这种情况下,   操作可以同步完成,返回一个具有的任务   已经完成。

所以我的最后一个答案是不正确的(出于性能原因,短时间异步操作 同步)。