13

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).

4

4 回答 4

11

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 while ReadLineAsync() 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 called await 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 the await.

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:

while ((line= await reader.ReadLineAsync()) != null)
{
    await Task.Factory.StartNew(ln =>
    {
        var lower = (ln as string).ToLowerInvariant();
        Console.WriteLine(lower);
     },line);
}
于 2013-07-10T12:23:04.030 回答
8

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 that ReadLineAsync 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:

await Task.Delay(1);

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:

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;
}

... 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.

于 2013-07-30T20:57:15.550 回答
2

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 the while loop, it does so on the SynchronizationContext that was captured on the await 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.

  1. In FileReadAllLinesAsync, the body of the while loop is not sensitive to which thread it runs on, so you can use the following instead:

    while ((await reader.ReadLineAsync().ConfigureAwait(false)) != null)
    
  2. In Main, suppose you do want the MessageBox.Show line to execute on the UI thread (perhaps you also have a buttonLoad.Enabled = true statement there). You can (and will!) still get this behavior without any changes to Main, since you did not use ConfigureAwait(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.

于 2013-08-01T12:29:02.960 回答
1

From Task-based Asynchronous Pattern in Microsoft Download Center :

For performance reasons, if a task has already completed by the time the task is awaited, control will not be yielded, and the function will instead continue executing.

And

In some cases, the amount of work required to complete the operation is less than the amount of work it would take to launch the operation asynchronously (e.g. reading from a stream where the read can be satisfied by data already buffered in memory). In such cases, the operation may complete synchronously, returning a Task that has already been completed.

So my last answer was incorrect (short-timing asynchronous operation is synchronous for performance reasons).

于 2013-07-30T10:34:09.957 回答