Почему File.ReadAllLinesAsync () блокирует поток пользовательского интерфейса?

Вот мой код. Обработчик событий для кнопки WPF, которая читает строки файла:

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
}

Я использовал асинхронную версию метода File.ReadAllLines() в приложении .NET Core 3.1 WPF.

Но он блокирует поток пользовательского интерфейса! Почему?


Обновление: так же, как @Theodor Zoulias, я провожу тест:

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

результат:

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

.NET Core 3.1, C # 8, WPF, отладочная сборка | 7,32 Мбайт Файл (.txt) | Жесткий диск 5400 SATA


person seraj    schedule 02.08.2020    source источник
comment
Отвечает ли это на ваш вопрос? Как выполнить Async Files.ReadAllLines и дождаться результатов?   -  person zaggler    schedule 02.08.2020
comment
@ Çöđěxěŕ - нет, этот вопрос касается синхронной версии ReadAllLines. Он использует асинхронную версию, и она все еще зависает (что должно быть невозможно, поэтому я предполагаю, что что-то еще вызывает проблемы)   -  person Andy    schedule 02.08.2020
comment
Что вы делаете с текстовым файлом, который читаете, и насколько он велик? Например, если вы делаете что-то вроде добавления текста в текстовое поле, как только он прочитает содержимое файла, он заблокирует поток пользовательского интерфейса при заполнении текстового поля.   -  person Rhys Wootton    schedule 02.08.2020
comment
Сам звонок не блокируется. Прокомментируйте все после звонка и убедитесь в этом сами.   -  person insane_developer    schedule 02.08.2020
comment
этот код блокирует пользовательский интерфейс, даже если я ничего не делаю с переменной строк.   -  person seraj    schedule 02.08.2020
comment
Мое предложение - используйте var lines = await Task.Run(() => File.ReadAllLines(@"D:\temp.txt"));, как предложено в ответе, это не только поддерживает отзывчивость пользовательского интерфейса, но и в 7 раз быстрее, чем ReadAllLinesAsync!   -  person aepot    schedule 03.08.2020
comment
@aepot, вы можете попробовать немного улучшить вопрос, например, исправив корпус и правильно отформатировав код. Это повысит вероятность того, что вопрос будет снова открыт (требуется еще один голос).   -  person Theodor Zoulias    schedule 03.08.2020


Ответы (2)


К сожалению, встроенные асинхронные API-интерфейсы для доступа к файловой системе не реализованы последовательно в соответствии с Microsoft собственные рекомендации о предполагаемом поведении асинхронных методов.

Асинхронный метод, основанный на TAP, может синхронно выполнять небольшой объем работы, например проверку аргументов и запуск асинхронной операции, прежде чем он вернет результирующую задачу. Синхронная работа должна быть сведена к минимуму, чтобы асинхронный метод мог быстро возвращаться.

Такие методы, как StreamReader.ReadToEndAsync, не работают таким образом, и вместо этого блокирует текущий поток на значительное время перед возвратом неполного Task. Например, в моем более старом эксперименте с чтением файла размером 6 МБ из мой SSD, этот метод заблокировал вызывающий поток на 120 мс, вернув Task, который затем был завершен всего через 20 мс. Мое предложение - избегать использования API-интерфейсов асинхронной файловой системы из приложений с графическим интерфейсом пользователя и использовать вместо них синхронные API-интерфейсы, заключенные в _ 4_.

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

Обновление. Вот некоторые экспериментальные результаты с File.ReadAllLinesAsync:

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

Выход:

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

Метод File.ReadAllLinesAsync заблокировал текущий поток на 450 мс, а возвращенная задача завершилась через 5 мс. Эти измерения стабильны после нескольких прогонов.

.NET Core 3.1.3, C # 8, консольное приложение, сборка выпуска (отладчик не подключен), Windows 10, SSD Toshiba OCZ Arc 100 240 ГБ

person Theodor Zoulias    schedule 02.08.2020
comment
Хороший ответ, но вы выбрасываете ветку: Task.Run, не нужно. - person zaggler; 02.08.2020
comment
OP с использованием .NET Core 3.1 API File.ReadAllLinesAsync. Это действительно асинхронно. Task.Run() - это плохая практика, например, тратить впустую объединенный поток во время чтения мухи. Это не рекомендуется для операций, основанных на вводе-выводе. Кажется, проблема OP находится за пределами показанного кода. - person aepot; 02.08.2020
comment
это пример приложения, в котором нет другого кода. этот код блокирует пользовательский интерфейс, даже если я ничего не делаю с переменной строк. - person seraj; 02.08.2020
comment
@seraj вы имеете в виду свой исходный код или мое предложение использовать Task.Run? - person Theodor Zoulias; 02.08.2020
comment
поскольку вы предлагаете, Task.Run работает нормально, и использование Stream Reader тоже отлично работает. Благодарю. - person seraj; 02.08.2020
comment
@aepot Я обновил свой ответ экспериментальными результатами для File.ReadAllLinesAsync. Пожалуйста, попробуйте этот фрагмент кода на своем ПК и сообщите о своих результатах. Что касается Task.Run плохой практики, это абсолютно верно для приложений ASP.NET и почти неважно для приложений WinForms / WPF. Ценность сохранения отзывчивости пользовательского интерфейса полностью затмевает любые соображения об увеличении размера ThreadPool на один или два потока. - person Theodor Zoulias; 02.08.2020
comment
Проголосовали. Похоже на ошибку в .NET. Покопаюсь в исходном коде .NET Core на GitHub, интересно ... - person aepot; 02.08.2020
comment
Можете ли вы проголосовать за повторное открытие вопроса? Похоже, он уже отредактирован. У меня недостаточно репутации. Возможно, я выложу здесь какие-то результаты расследования проблемы, но это невозможно для закрытого вопроса. - person aepot; 02.08.2020
comment
Я закончил расследование и: Да! ReadAllLinesAsync - это полностью СЛОМАННЫЙ метод API. У меня довольно быстрый SSD, и я тестировал его с текстовым файлом размером 12 МБ (простой json внутри). Результаты сломали мне мозг. Это потрясающе: ReadAllLinesAsync = 350 мс с зависанием пользовательского интерфейса, Task.Run => ReadAllLines (вызов синхронизации в Task) - 55 мс. Асинхронный в 7 раз медленнее! Смущенный. И наконец: вот ошибка. Я думаю, что это повод добавить ответ с этой информацией и этой справкой. - person aepot; 03.08.2020
comment
Та же ошибка для ReadAllTextAsync, WriteAllLinesAsync и WriteAllTextAsync. Не ожидал этого от .NET. Все тесты проводились на .NET Core 3.1. - person aepot; 03.08.2020
comment
@aepot yeap, очень грустно, что API асинхронной файловой системы сломан. Некоторое время назад я написал свои аргументы в пользу использование Task.Run в обработчиках событий приложений с графическим интерфейсом пользователя, за исключением встроенных асинхронных API, поскольку они реализованы экспертами. Мало ли я знаю... - person Theodor Zoulias; 03.08.2020
comment
@TheodorZoulias, мы снова встречаемся! Я полностью согласен с упаковкой того, что вам нужно делать с API-интерфейсом Task.Run () специально для настольных приложений Windows. Он лучше работает в файловых системах на базе Linux, но в настоящее время у них нет компонентов пользовательского интерфейса, за исключением консоли. В качестве альтернативы - потоковое исполнение построчно асинхронно с помощью IAsyncEnumerable! - person HouseCat; 03.08.2020
comment
@HouseCat, транслирующий строки как IAsyncEnumerable, может сделать приложение более отзывчивым, но общая производительность чтения всех строк, скорее всего, будет хуже, потому что асинхронная работа приводит к увеличению накладных расходов. - person Theodor Zoulias; 03.08.2020
comment
@TheodorZoulias да, потоковая передача каждой строки за строкой - в целом будет медленнее. Преимущество потоковой передачи здесь заключается в том, что у вас есть возможность обрабатывать одну строку за раз (выделенная память), что отлично подходит для микросервисов :) - person HouseCat; 03.08.2020

Спасибо Теодору Зулиасу за ответ, он правильный и работает.

Ожидая асинхронного метода, текущий поток будет ждать результата асинхронного метода. Текущий поток в этом случае является основным потоком, поэтому он ожидает результата процесса чтения и, таким образом, замораживает пользовательский интерфейс. (UI обрабатывается основным потоком)

Чтобы поделиться дополнительной информацией с другими пользователями, я создал решение Visual Studio, чтобы практически реализовать идеи.

Проблема: асинхронно считайте большой файл и обработайте его, не останавливая пользовательский интерфейс.

Случай 1: если это случается редко, я рекомендую создать цепочку и прочитать содержимое файла, обработать файл и затем закрыть цепочку. Используйте приведенные ниже строки кода из события нажатия кнопки.

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

Случай 2: если это происходит постоянно, я рекомендую создать канал и подписаться на него в фоновом потоке. всякий раз, когда публикуется новое имя файла, потребитель будет читать его асинхронно и обрабатывать.

Архитектура:  Архитектура канала

Вызовите метод (InitializeChannelReader) в вашем конструкторе, чтобы подписаться на канал.

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

Вызвать метод метода, чтобы опубликовать имя файла в канале, который будет использоваться потребителем. Используйте приведенные ниже строки кода из события нажатия кнопки.

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);
person Saeed Aghdam    schedule 14.06.2021