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

Use*_*678 12 c# asynchronous visual-studio 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);
}
Run Code Online (Sandbox Code Playgroud)

我希望列出的代码能够异步运行,事实上,确实如此!但只有在没有 Visual Studio Debugger的情况下运行代码时.

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

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

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

Pan*_*vos 11

问题是你await reader.ReadLineAsync()在一个没有做任何事情的紧密循环中调用- 除了在每次等待之后返回执行到UI线程之后再重新开始.您的UI线程可以在ReadLineAsync()尝试读取行时自由处理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);
}
Run Code Online (Sandbox Code Playgroud)


Jon*_*eet 8

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

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

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

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

希望通过网络运行相同的代码将"修复"的问题,但它似乎没有.(请注意,它会改变如何显示急躁情绪.)但是,使用:

await Task.Delay(1);
Run Code Online (Sandbox Code Playgroud)

请问解冻的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;
}
Run Code Online (Sandbox Code Playgroud)

...并为UI创建并添加合适的标签.在我的机器上,无论是在调试还是非调试中,我都看到比"不完整"更"完整"的命中 - 奇怪的是,完整与不完整的比例始终是84:1,都在调试器下,而不是.因此,它只能读到一个在85线的UI后是可以得到一个机会来更新.您应该在您的机器上尝试相同的操作.

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