CancellationToken и таймауты
↑ 2.3 Асинхронность и многопоточность · ← Предыдущая · → Следующая
Зачем это на собесе
Отмена и таймауты — признак зрелого бэкенд-кода. Спросят, как прервать долгий запрос, когда клиент закрыл соединение, как задать таймаут на операцию, чем
CancellationTokenSourceотличается от токена и что делать сOperationCanceledException.
Объяснение
Кооперативная отмена
Отмена в .NET кооперативная: одна сторона просит остановиться, другая сама проверяет и завершается.
CancellationTokenSource(CTS) — источник: вызываетCancel().CancellationToken— токен: передаётся в методы и проверяется.
using var cts = new CancellationTokenSource();
var task = ProcessAsync(cts.Token);
cts.Cancel(); // просьба остановиться
try { await task; } catch (OperationCanceledException) { /* штатная отмена */ }
async Task ProcessAsync(CancellationToken ct)
{
foreach (var item in items)
{
ct.ThrowIfCancellationRequested(); // проверка между шагами
await HandleAsync(item, ct); // передаём токен вниз
}
}Что делать в коде
- Принимать
CancellationToken ctпоследним параметром (по умолчанию= default) во всех асинхронных методах. - Передавать его в каждый вложенный вызов:
HttpClient, EF Core (ToListAsync(ct)),Task.Delay,Stream.ReadAsync. - Проверять в собственных циклах:
ct.ThrowIfCancellationRequested()илиct.IsCancellationRequested. OperationCanceledException(иTaskCanceledException) — штатная реакция на отмену, не ошибка: не логируйте как error.
В ASP.NET Core
app.MapGet("/api/report/{id}", async (Guid id, IReportService svc, CancellationToken ct) =>
Results.File(await svc.BuildAsync(id, ct), "application/pdf"));Токен запроса (HttpContext.RequestAborted) срабатывает, если клиент закрыл соединение. В контроллерах и Minimal API его можно просто добавить параметром.
Таймауты
// 1) на группу операций: связанный токен
using var timeout = new CancellationTokenSource(TimeSpan.FromSeconds(5));
using var linked = CancellationTokenSource.CreateLinkedTokenSource(ct, timeout.Token); // отмена клиента ИЛИ таймаут
var data = await client.GetAsync(url, linked.Token);
// 2) CancelAfter
cts.CancelAfter(TimeSpan.FromSeconds(30));
// 3) .NET 6+: ограничить время ожидания задачи (сама операция не отменяется!)
var result = await slowTask.WaitAsync(TimeSpan.FromSeconds(5), ct);
// 4) HttpClient.Timeout (по умолчанию 100 с) и политики Polly / Microsoft.Extensions.Http.Resilience
builder.Services.AddHttpClient<Api>().AddStandardResilienceHandler();Различать причину: catch (OperationCanceledException) when (timeout.IsCancellationRequested) — таймаут; when (ct.IsCancellationRequested) — отмена вызывающего.
Регистрация обратных вызовов и освобождение ресурсов
await using var registration = ct.Register(() => connection.Cancel()); // отменить нативный ресурсCancellationTokenSource держит таймер и регистрации: освобождайте (Dispose), особенно при CancelAfter.
Отмена в фоновых сервисах
BackgroundService.ExecuteAsync(CancellationToken stoppingToken) — токен срабатывает при остановке приложения (SIGTERM). Все циклы и задержки должны его учитывать, иначе будет принудительное завершение по истечении ShutdownTimeout.
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
using var timer = new PeriodicTimer(TimeSpan.FromSeconds(10));
while (await timer.WaitForNextTickAsync(stoppingToken))
await DoWorkAsync(stoppingToken);
}Нюансы и подводные камни
- Отмена ничего не гарантирует: если код не проверяет токен, он продолжит работу. Метод без токена нельзя отменить.
WaitAsync(timeout)не отменяет саму операцию: она продолжит выполняться в фоне. Для реальной остановки нужен токен в самой операции.- Отмена после
commitтранзакции или отправки сообщения не откатывает результат: продумайте идемпотентность. Task.Delayбез токена — «зависший» таймер и задержка остановки приложения.- Ловить
OperationCanceledExceptionслишком широко (catch (Exception)) — глушить отмену; отделяйте. CancellationToken.Noneв цепочке «разрывает» отмену: клиент ушёл, а запрос к БД продолжается.- Не передавайте токен запроса в фоновую работу, которая должна пережить запрос: он отменится вместе с запросом.
Практика
- Добавить токен во все методы сервиса и проверить, что закрытие соединения клиентом обрывает запрос в БД.
- Реализовать метод с общим таймаутом на несколько вложенных вызовов через
CreateLinkedTokenSource. - Найти в проекте
Task.Delayбез токена и исправить.
Вопросы с ответами
Как работает отмена в .NET?
Кооперативно:
CancellationTokenSourceвыставляет запрос на отмену, а код, получившийCancellationToken, сам проверяет его или передаёт в вызовы, поддерживающие отмену. Обычно завершение выражаетсяOperationCanceledException.
Чем
CancellationTokenотличается отCancellationTokenSource?Токен только читается и передаётся в методы. Источник управляет отменой (
Cancel,CancelAfter) и выдаёт токены.
Как реализовать таймаут на группу операций и при этом учитывать отмену клиента?
Создать
CancellationTokenSourceс таймаутом и связать с исходным токеном черезCreateLinkedTokenSource; передавать связанный токен вниз.
Должна ли отмена логироваться как ошибка?
Нет. Это штатное поведение (клиент закрыл соединение, приложение останавливается). Ловите
OperationCanceledExceptionотдельно и не поднимайте уровень error.
Чем
WaitAsync(timeout)отличается от токена с таймаутом?
WaitAsyncпрекращает ожидание, но операция продолжает работать в фоне. Токен с таймаутом действительно просит операцию остановиться, если она его учитывает.
Связанные темы
- Предыдущая: ValueTask и аллокации · Следующая: Синхронизация: lock, Monitor, SemaphoreSlim, Interlocked, ReaderWriterLockSlim
- Task и TPL: Task и TPL
- Фоновые задачи: 3.5 Инфраструктура приложения: ошибки, health checks, фоновые задачи
- Устойчивость: retry, timeout, circuit breaker: 7.3 Надёжность и отказоустойчивость
- AbortController в JS: Асинхронность: callbacks, Promise, async∕await