Что такое таски и async/await в C#?
Что проверяют
Понимаете ли вы, что асинхронность в C# это не про создание потоков, а про то, чтобы поток не стоял в ожидании. Дальше почти всегда идёт продолжение про пул потоков, взаимоблокировки и отмену, поэтому определения вида «async делает код асинхронным» не хватает.
Как отвечать
Task это объект, представляющий уже запущенную операцию и её будущий результат: состояние, значение или исключение, плюс возможность подписать продолжение. async и await это синтаксис, который компилятор разворачивает в конечный автомат.
Код до первого await выполняется синхронно, в вызывающем потоке. На await метод проверяет, готов ли результат, и если нет, возвращает управление вызывающему, а остаток метода регистрируется как продолжение и выполнится по завершении операции.
Важный акцент: при ожидании ввода-вывода никакой поток не занят. Запрос отдан операционной системе, продолжение вызовет её механизм завершения. Поэтому Task.Run вокруг сетевого вызова бессмыслен, он занимает поток пула ожиданием того, чего ждать не требуется. Уместен Task.Run для счётной работы, которую надо убрать из потока интерфейса.
Что ещё стоит назвать: возвращать Task или Task<T>, а async void оставить только обработчикам событий; ValueTask для горячих путей, где результат обычно готов; CancellationToken параметром до самого низа; исключение не теряется, а сохраняется в задаче и выбрасывается на await.
Где спотыкаются
Ждут результат синхронно через .Result или .Wait(). В коде с контекстом синхронизации это классическая взаимоблокировка, в остальных случаях просто занятый поток пула и голодание под нагрузкой.
Пишут async void не в обработчике события. Вызывающий не может ни дождаться такого метода, ни поймать его исключение: оно улетает мимо и роняет процесс.
Забывают await у возвращённой задачи. Метод уходит дальше, не дождавшись записи, исключение тонет в невостребованной задаче, а ошибка проявляется позже и совсем в другом месте.