7

Here is my code. An event handler for WPF button that reads lines of a file:

private async void Button_OnClick(object sender, RoutedEventArgs e)
{
    Button.Content = "Loading...";
    var lines = await File.ReadAllLinesAsync(@"D:\temp.txt"); //Why blocking UI Thread???
    Button.Content = "Show"; //Reset Button text
}

I used asynchronous version of File.ReadAllLines() method in .NET Core 3.1 WPF App.

But it is blocking the UI Thread! Why?


Update: Same as @Theodor Zoulias, I do a test :

private async void Button_OnClick(object sender, RoutedEventArgs e)
    {
        Button.Content = "Loading...";
        TextBox.Text = "";

        var stopwatch = Stopwatch.StartNew();
        var task = File.ReadAllLinesAsync(@"D:\temp.txt"); //Problem
        var duration1 = stopwatch.ElapsedMilliseconds;
        var isCompleted = task.IsCompleted;
        stopwatch.Restart();
        var lines = await task;
        var duration2 = stopwatch.ElapsedMilliseconds;

        Debug.WriteLine($"Create: {duration1:#,0} msec, Task.IsCompleted: {isCompleted}");
        Debug.WriteLine($"Await:  {duration2:#,0} msec, Lines: {lines.Length:#,0}");


        Button.Content = "Show";
    }

result is :

Create: 652 msec msec, Task.IsCompleted: False | Await:   15 msec, Lines: 480,001

.NET Core 3.1, C# 8, WPF, Debug build | 7.32 Mb File(.txt) | HDD 5400 SATA

Theodor Zoulias
  • 34,835
  • 7
  • 69
  • 104
seraj
  • 149
  • 1
  • 7
  • Does this answer your question? [How to Async Files.ReadAllLines and await for results?](https://stackoverflow.com/questions/13167934/how-to-async-files-readalllines-and-await-for-results) – Trevor Aug 02 '20 at 15:26
  • @Çöđěxěŕ - no, that question deals with the synchronous version of `ReadAllLines`. He is using the Asynchronous version and it still hangs (which should be impossible, so i am going to assume something else is causing issues) – Andy Aug 02 '20 at 15:55
  • 1
    What are you doing with the text file you are reading in, and how big is it? For example, if you are doing something like adding the text to a textbox as soon as it has read the contents of the file, it will block the UI thread whilst populating the textbox. – Rhys Wootton Aug 02 '20 at 16:04
  • 2
    The call itself does not block. Comment out everything after the call and see for yourself. – insane_developer Aug 02 '20 at 16:08
  • this code blocks ui even if i do nothing with lines variable. – seraj Aug 02 '20 at 17:39
  • 2
    My suggestion - use `var lines = await Task.Run(() => File.ReadAllLines(@"D:\temp.txt"));` as suggested in answer, it's not only keeps UI responsive, it's [7x faster](https://stackoverflow.com/q/51560443/12888024) than `ReadAllLinesAsync`! – aepot Aug 02 '20 at 21:42

2 Answers2

13

Sadly currently (.NET 5) the built-in asynchronous APIs for accessing the filesystem are not implemented consistently according to Microsoft's own recommendations about how asynchronous methods are expected to behave.

An asynchronous method that is based on TAP can do a small amount of work synchronously, such as validating arguments and initiating the asynchronous operation, before it returns the resulting task. Synchronous work should be kept to the minimum so the asynchronous method can return quickly.

Methods like StreamReader.ReadToEndAsync do not behave this way, and instead block the current thread for a considerable amount of time before returning an incomplete Task. For example in an older experiment of mine with reading a 6MB file from my SSD, this method blocked the calling thread for 120 msec, returning a Task that was then completed after only 20 msec. My suggestion is to avoid using the asynchronous filesystem APIs from GUI applications, and use instead the synchronous APIs wrapped in Task.Run.

var lines = await Task.Run(() => File.ReadAllLines(@"D:\temp.txt"));

Update: Here are some experimental results with File.ReadAllLinesAsync:

Stopwatch stopwatch = Stopwatch.StartNew();
Task<string[]> task = File.ReadAllLinesAsync(@"C:\6MBfile.txt");
long duration1 = stopwatch.ElapsedMilliseconds;
bool isCompleted = task.IsCompleted;
stopwatch.Restart();
string[] lines = await task;
long duration2 = stopwatch.ElapsedMilliseconds;
Console.WriteLine($"Create: {duration1:#,0} msec, Task.IsCompleted: {isCompleted}");
Console.WriteLine($"Await:  {duration2:#,0} msec, Lines: {lines.Length:#,0}");

Output:

Create: 450 msec, Task.IsCompleted: False
Await:  5 msec, Lines: 204,000

The method File.ReadAllLinesAsync blocked the current thread for 450 msec, and the returned task completed after 5 msec. These measurements are consistent after multiple runs.

.NET Core 3.1.3, C# 8, Console App, Release build (no debugger attached), Windows 10, SSD Toshiba OCZ Arc 100 240GB


.NET 6 update. The same test on the same hardware using .NET 6:

Create: 19 msec, Task.IsCompleted: False
Await:  366 msec, Lines: 204,000

The implementation of the asynchronous filesystem APIs has been improved on .NET 6, but still they are far behind the synchronous APIs (they are about 2 times slower, and not totally asynchronous). So my suggestion to use the synchronous APIs wrapped in Task.Run still holds.

Theodor Zoulias
  • 34,835
  • 7
  • 69
  • 104
  • Good answer, but you're throwing away a thread: `Task.Run`, don't need to. – Trevor Aug 02 '20 at 17:20
  • 2
    OP using .NET Core 3.1 API `File.ReadAllLinesAsync`. It's truly asynchronous. `Task.Run()` is bad practice here like wasting a pooled Thread while the flie is reading, It's not recommended for I/O based operations. Seems the OP's problem is outside of the shown code. – aepot Aug 02 '20 at 17:24
  • this is a sample app and doesn't have any other code. this code blocks ui even if i do nothing with lines variable. – seraj Aug 02 '20 at 17:44
  • 1
    @seraj are you referring to your original code, or to my suggestion of using `Task.Run`? – Theodor Zoulias Aug 02 '20 at 17:49
  • 1
    as u suggest Task.Run Works Fine and Using Stream Reader is works fine too. thanks. – seraj Aug 02 '20 at 18:10
  • 3
    @aepot I updated my answer with experimental results for `File.ReadAllLinesAsync`. Please try this code snippet in your PC and report your results. Regarding `Task.Run` being bad practice, this is absolutely true for ASP.NET applications, and almost unimportant for WinForms/WPF applications. The value of keeping the UI responsive completely dwarfs any considerations about increasing the size of the `ThreadPool` by one thread or two. – Theodor Zoulias Aug 02 '20 at 18:28
  • 1
    Upvoted. Looks like a bug in .NET. I will dig into .NET Core source code at GitHub, interesting... – aepot Aug 02 '20 at 18:41
  • Can you vote to reopen the question? Looks like it's already edited. I have no enough rep. Maybe I'll post here some results of investigation of the issue, but it's not possible for closed question. – aepot Aug 02 '20 at 18:45
  • 2
    I've completed the investigation and: Yes! `ReadAllLinesAsync` is totally BROKEN API method. I have pretty fast SSD and tested with 12MB text file (simple json inside). Results made my brain broken. That's awesome: `ReadAllLinesAsync` = 350ms with UI freeze, `Task.Run => ReadAllLines` (sync call in `Task`) - 55ms. **Async** is 7x times slower! Confused. **And finally:** [here's a Bug](https://github.com/dotnet/runtime/issues/27047). I think that's a reason to append an answer with this info and this [reference](https://stackoverflow.com/q/51560443/12888024). – aepot Aug 02 '20 at 21:26
  • 2
    Same bug for `ReadAllTextAsync`, `WriteAllLinesAsync` and `WriteAllTextAsync`. Didn't expect that from .NET. All tests preformed on .NET Core 3.1. – aepot Aug 02 '20 at 21:51
  • 2
    @aepot yeap, it is quite sad that the async filesystem API is broken. Some time ago I wrote [my arguments](https://stackoverflow.com/questions/38739403/await-task-run-vs-await-c-sharp/58306020#58306020) in favor of using `Task.Run` in event handlers of GUI applications, excluding the built-in async APIs because they *"are implemented by experts"*. Little did I know... – Theodor Zoulias Aug 02 '20 at 21:58
  • 1
    @TheodorZoulias we meet a again! I fully agree in wrapping what you need to do with Task.Run() apis on Windows desktop apps specifically. It works better on Linux based file systems but they don't have UI components save for the console at this time. Alternatively - stream line by line asynchronously using IAsyncEnumerable! – HouseCat Aug 03 '20 at 13:17
  • 1
    @HouseCat streaming the lines as an `IAsyncEnumerable` can make the app more responsive, but the overall performance of reading all lines will most probably be worse, because making things work asynchronously adds overhead. – Theodor Zoulias Aug 03 '20 at 13:38
  • 2
    @TheodorZoulias yep streaming every line by line - holistically will be slower. The benefit from streaming lines here is that you have option of processing one line at a time (memory allocated) which is great for microservices :) – HouseCat Aug 03 '20 at 13:48
0

Thanks to Theodor Zoulias for the answer, it's correct and working.

When awaiting an async method, the current thread will wait for the result of the async method. The current thread in this case is main thread, so it's wait for the result of the reading process and thus freeze the UI. (UI is handle by the main thread)

To share more information with other users, I created a visual studio solution to give the ideas practically.

Problem: Read a huge file async and process it without freezing the UI.

Case1: If it happens rarely, my recommendation is to create a thread and read the content of file, process the file and then kill the thread. Use the bellow lines of code from the button's on-click event.

OpenFileDialog fileDialog = new OpenFileDialog()
{
    Multiselect = false,
    Filter = "All files (*.*)|*.*"
};
var b = fileDialog.ShowDialog();
if (string.IsNullOrEmpty(fileDialog.FileName))
    return;

Task.Run(async () =>
{
    var fileContent = await File.ReadAllLinesAsync(fileDialog.FileName, Encoding.UTF8);

    // Process the file content
    label1.Invoke((MethodInvoker)delegate
    {
        label1.Text = fileContent.Length.ToString();
    });
});

Case2: If it happens continuously, my recommendation is to create a channel and subscribe to it in a background thread. whenever a new file name published, the consumer will read it asynchronously and process it.

Architecture: Channel architecture

Call below method (InitializeChannelReader) in your constructor to subscribe to channel.

private async Task InitializeChannelReader(CancellationToken cancellationToken)
{
    do
    {
        var newFileName = await _newFilesChannel.Reader.ReadAsync(cancellationToken);
        var fileContent = await File.ReadAllLinesAsync(newFileName, Encoding.UTF8);

        // Process the file content
        label1.Invoke((MethodInvoker)delegate
        {
            label1.Text = fileContent.Length.ToString();
        });
    } while (!cancellationToken.IsCancellationRequested);
}

Call method method in order to publish file name to channel which will be consumed by consumer. Use the bellow lines of code from the button's on-click event.

OpenFileDialog fileDialog = new OpenFileDialog()
{
    Multiselect = false,
    Filter = "All files (*.*)|*.*"
};
var b = fileDialog.ShowDialog();
if (string.IsNullOrEmpty(fileDialog.FileName))
    return;

await _newFilesChannel.Writer.WriteAsync(fileDialog.FileName);
Saeed Aghdam
  • 307
  • 3
  • 11