Как обеспечить отзывчивость интерфейса во время длительных операций в C#
Что проверяют
Понимаете ли вы, что интерфейс замирает из-за занятого UI-потока, и различаете ли ожидание ввода-вывода и тяжёлые вычисления. Следом обычно спрашивают про взаимоблокировку при .Result, прогресс и отмену.
Как отвечать
В WinForms, WPF и MAUI интерфейс обслуживает один поток с циклом сообщений. Пока обработчик события выполняется синхронно, окно не перерисовывается и не реагирует на клики. Задача в том, чтобы вернуть поток циклу как можно быстрее:
- ввод-вывод через
async/await: сеть, диск, база.awaitне занимает поток на время ожидания, а продолжение возвращается в UI-поток черезSynchronizationContext, поэтому после него можно трогать контролы; - вычисления через
Task.Run: разбор большого файла, обработка изображения. Сам по себеasyncвычисление в фон не уводит, код до первогоawaitидёт синхронно в UI-потоке; - прогресс через
IProgress<T>: экземплярProgress<T>, созданный в UI-потоке, сам доставляетReportобратно в него. Сообщайте раз в N элементов, а не на каждый, иначе завалите очередь сообщений; - отмена через
CancellationToken:CancellationTokenSourceна кнопке «Отмена», токен передаётся во все асинхронные вызовы, а в цикле вычислений проверяетсяThrowIfCancellationRequested(); - состояние интерфейса: блокируйте кнопку на время операции, показывайте индикатор, разблокируйте в
finally.
В библиотечном коде ставят ConfigureAwait(false), чтобы не тянуть продолжения в UI-поток. HttpClient держат один на приложение или берут из IHttpClientFactory, а не создают на каждый клик.
Где спотыкаются
Вызывают .Result или .Wait() у задачи из UI-потока. Поток заблокирован, продолжение ждёт этот же поток, и приложение виснет навсегда.
Пишут async void не только в обработчиках событий и не оборачивают тело в try/catch. Вызывающий код такое исключение не поймает, оно уходит прямо в контекст синхронизации и чаще всего роняет приложение.
Заворачивают в Task.Run вызов, который и так асинхронный, или наоборот трогают контрол из фонового потока. В первом случае впустую занят поток пула, во втором вылетает InvalidOperationException о доступе из чужого потока.