我正在尝试使用异步功能实现一个名为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)上进行的。
我想知道为什么会出现这个问题以及如何解决它(因为没有调试器的运行可能会阻碍开发)。
答案 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线程上执行的消息。具体来说,当FileReadAllLinesAsync
在while
循环的正文中继续执行时,它会在同一方法中SynchronizationContext
行上捕获的await
上执行此操作。这意味着对于文件中的每一行,都会将一条消息发回到UI线程,以执行该while循环体的1个副本。
您可以谨慎使用ConfigureAwait(false)
来解决此问题。
在FileReadAllLinesAsync
中,while循环的主体对其运行的线程不敏感,因此您可以使用以下代码:
while ((await reader.ReadLineAsync().ConfigureAwait(false)) != null)
在Main
中,假设您做想要在UI线程上执行MessageBox.Show
行(也许您还有一个buttonLoad.Enabled = true
语句)。您可以(而且会!)仍然可以在不对Main
进行任何更改的情况下获得此行为,因为您没有在那里使用ConfigureAwait(false)
。
我怀疑您在调试器中观察到的延迟是由于.NET在托管/非托管代码中连接调试器时性能较慢,因此将这些数百万条消息中的每条消息分发到UI线程时,速度会慢100倍调试器附加。我怀疑上面的第1项将立即解决您的大部分问题,而不是试图通过禁用功能加快调度速度。
答案 3 :(得分:1)
从Microsoft下载中心的Task-based Asynchronous Pattern:
出于性能原因,如果任务已经完成了 等待任务,控制不会产生和功能 将改为继续执行。
和
在某些情况下,完成操作所需的工作量 小于启动操作所需的工作量 异步(例如,从可以读取的流中读取) 对已经缓冲在内存中的数据感到满意。在这种情况下, 操作可以同步完成,返回一个具有的任务 已经完成。
所以我的最后一个答案是不正确的(出于性能原因,短时间异步操作 同步)。