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

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

 private static async Task<IEnumerable> 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 Debugger的情况下运行代码时。

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

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

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

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

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

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

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

我原本以为通过网络运行相同的代码会“修复”问题,但似乎并没有。 (请注意,它会改变如何显示急动。)但是,使用:

 await Task.Delay(1); 

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

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

这些只是猜测,但它们至少在某种程度上受过教育。

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

 private static async Task> 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,对于相同的行数。

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

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

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

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

即使它不仅仅是一个紧密的循环,你的原始代码也会阻塞,因为处理数据的循环中的任何代码仍将在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); } 

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

您可以通过仔细使用ConfigureAwait(false)来解决此问题。

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

     while ((await reader.ReadLineAsync().ConfigureAwait(false)) != null) 
  2. Main ,假设您确实希望MessageBox.Show行在UI线程上执行(也许您还有一个buttonLoad.Enabled = true语句)。 你可以(而且会!)仍然得到这种行为而不对Main进行任何更改,因为你没有在那里使用ConfigureAwait(false)

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

从Microsoft下载中心的基于任务的异步模式 :

出于性能原因,如果任务在等待任务时已经完成,则不会产生控制,而是该函数将继续执行。

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

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