Как работают Task под капотом в C#?
Что проверяют
Понимаете ли вы, что Task это обещание результата, а не поток, и что компилятор делает с async. Следом почти всегда спрашивают, где выполнится код после await и почему .Result может повесить приложение.
Как отвечать
Taskэто объект состояния: статус (WaitingForActivation,Running,RanToCompletion,Faulted,Canceled), результат или исключение и список продолжений. Потока за задачей может не быть вовсе:Task.Delayдержится на таймере, сетевой запрос на асинхронном вводе-выводе ОС, и ни один поток при этом не ждёт;Task.Run: ставит делегат вThreadPoolчерезTaskScheduler.Default. Это инструмент для CPU-работы, а не способ «сделать метод асинхронным»;async-метод компилятор превращает в конечный автомат, реализующийIAsyncStateMachine, а локальные переменные становятся его полями. Наawaitберётся awaiter: если задача уже завершена, код идёт дальше синхронно, если нет, регистрируется продолжение, и метод возвращает управление;- контекст: продолжение по умолчанию возвращается в захваченный
SynchronizationContext, например на UI-поток в WPF. В ASP.NET Core такого контекста нет, а в библиотеках пишутConfigureAwait(false); TaskCompletionSource<T>: задача без делегата, которую вы завершаете сами. Так оборачивают API на колбэках;- исключения:
awaitвыбрасывает первое исключение как есть, а.Wait()и.Resultзаворачивают его вAggregateException; ValueTask: для горячих путей, где результат часто готов синхронно и лишняя аллокация заметна.
Где спотыкаются
Говорят «каждая Task это новый поток». Для операций ввода-вывода это неверно, и именно понимание этого отличает хороший ответ.
Блокируют асинхронный код через .Result или .Wait(). При наличии контекста синхронизации это классический дедлок: поток ждёт задачу, а её продолжение ждёт этот же поток. В ASP.NET Core дедлока нет, но такие блокировки под нагрузкой выедают пул потоков.
Пишут async void вне обработчиков событий. Такой метод нельзя дождаться, исключение из него не поймать через try/catch в вызывающем коде, и оно роняет процесс.