I am trying to implement a method called ReadAllLinesAsync
using the async feature. I have produced the following code:
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);
}
I expect the listed code to run asynchronously and as a matter of fact, it does! But only when I run the code without the Visual Studio Debugger.
When I run the code with the Visual Studio Debugger attached, the code runs synchronously, blocking the main thread causing the UI to hang.
I have attempted and succeeded to reproduce the problem on three machines. Each test was conducted on a 64bit machine (either Windows 8 or Windows 7) using Visual Studio 2012.
I would like to know why this problem is occuring and how to solve it (as running without the debugger will likely hinder development).
From Task-based Asynchronous Pattern in Microsoft Download Center :
And
So my last answer was incorrect (short-timing asynchronous operation is synchronous for performance reasons).
The problem is that you are calling
await reader.ReadLineAsync()
in a tight loop that does nothing - except return execution to the UI thread after each await before starting all over again. Your UI thread is free to process windows events ONLY whileReadLineAsync()
tries to read a line.To fix this, you can change the call to
await reader.ReadLineAsync().ConfigureAwait(false)
.await
waits for the completion of an asynchronous call and returns execution to the Syncrhonization context that calledawait
in the first place. In a desktop application, this is the UI thread. This is a good thing because it allows you to update the UI directly but can cause blocking if you process the results of the asynchronous call right after theawait
.You can change this behavior by specifying
ConfigureAwait(false)
in which case execution continues in a different thread, not the original Synchronization context.Your original code would block even if it wasn't just a tight loop, as any code in the loop that processed the data would still execute in the UI thread. To process the data asynchronously without adding
ConfigureAwait
, you should process the data in a taks created using eg. Task.Factory.StartNew and await that task.The following code will not block because processing is done in a different thread, allowing the UI thread to process events:
I'm seeing the same problem as you to an extent - but only to an extent. For me, the UI is very jerky in the debugger, and occasionally jerky not in the debugger. (My file consists of lots of lines of 10 characters, by the way - the shape of the data will change behaviour here.) Often in the debugger it's good to start with, then bad for a long time, then it sometimes recovers.
I suspect the problem may simply be that your disk is too fast and your lines are too short. I know that sounds crazy, so let me explain...
When you use an
await
expression, that will only go through the "attach a continuation" path if it needs to. If the results are present already, the code just extracts the value and continues in the same thread.That means, if
ReadLineAsync
always returns a task which is completed by the time it returns, you'll effectively see synchronous behaviour. It's entirely possible thatReadLineAsync
looks at what data it's already got buffered, and tries to synchronously find a line within it to start with. The operating system may well then read more data from the disk so that it's ready for your application to use... which means that the UI thread never gets a chance to pump its normal messages, so the UI freezes.I had expected that running the same code over a network would "fix" the problem, but it didn't seem to. (It changes exactly how the jerkiness is shown, mind you.) However, using:
Does unfreeze the UI. (
Task.Yield
doesn't though, which again confuses me a lot. I suspect that may be a matter of prioritization between the continuation and other UI events.)Now as for why you're only seeing this in the debugger - that still confuses me. Perhaps it's something to do with how interrupts are processed in the debugger, changing the timing subtly.
These are only guesses, but they're at least somewhat educated ones.
EDIT: Okay, I've worked out a way to indicate that it's at least partly to do with that. Change your method like this:
... and create and add a suitable label to the UI. On my machine, both in debug and non-debug, I see far more "complete" hits than "incomplete" - oddly enough, the ratio of complete to incomplete is 84:1 consistently, both under the debugger and not. So it's only after reading about one in 85 lines that the UI can get a chance to update. You should try the same on your machine.
As another test, I added a counter incrementing in the
label.Paint
event - in the debugger it only executed 1/10th as many times as not in the debugger, for the same number of lines.Visual Studio isn't actually executing the asynchronous callback synchronously. However, your code is structured in such a manner that it is "flooding" the UI thread with messages that you may not need to execute on a UI thread. Specifically, when
FileReadAllLinesAsync
resumes execution in the body of thewhile
loop, it does so on theSynchronizationContext
that was captured on theawait
line in the same method. What this means is for every line in your file, a message is posted back to the UI thread to execute 1 copy of the body of that while loop.You can resolve this issue by using
ConfigureAwait(false)
carefully.In
FileReadAllLinesAsync
, the body of the while loop is not sensitive to which thread it runs on, so you can use the following instead:In
Main
, suppose you do want theMessageBox.Show
line to execute on the UI thread (perhaps you also have abuttonLoad.Enabled = true
statement there). You can (and will!) still get this behavior without any changes toMain
, since you did not useConfigureAwait(false)
there.I suspect the delays you observe in the debugger are due to .NET's slower performance in managed/unmanaged code while a debugger is attached, so dispatching each of those millions of messages to the UI thread is up to 100x slower when you have the debugger attached. Rather than try to speed up that dispatching by disabling features, I suspect item #1 above will resolve the bulk of your problems immediately.