Отмена
Отмена кооперативная. Dart не может прервать произвольный await,
и отметка об отмене Job не останавливает её ввод-вывод. Контекст даёт
телу контрольные точки, в которых оно отвечает на отмену, и выбранная
точка решает, что станет с операцией за ней:
(ctx) async { // Ожидание кончается в момент принятия отмены. Запрос может быть ещё // в пути; то, что он вернёт, будет отброшено. final name = await ctx.wait(() => api.load(id));
// Этого дожидаются в любом случае, и только потом вместо значения // выходит отмена. final handle = await ctx.join(() => device.open()); ctx.onDispose(handle.close);
// Пока это выполняется, Job не помечается вовсе. await ctx.uncancellable(() => payment.commit());
// Оборачивать нечего: контрольная точка сама по себе. ctx.check(); print(name);}| Метод | Если Job принимает отмену во время ожидания |
|---|---|
ctx.wait(action) |
Бросает Cancelled, не дожидаясь завершения операции. |
ctx.join(action) |
Ждёт операцию; проверяет отмену перед возвратом успешного результата. |
ctx.uncancellable(action) |
Удерживает обычную отмену до завершения действия; её бросает следующая контрольная точка. |
ctx.check() |
Бросает Cancelled, если Job уже отменена или её правила больше не выполняются. |
wait подходит для запроса, результат которого можно отбросить. Запрос
может продолжаться после завершения Job и старта следующей.
join подходит для работы, которая должна закончиться до продвижения
очереди: например, команды устройству или освобождения ресурса.
Ни один из методов не останавливает саму операцию.
Если операция, ожидаемая через join, завершается ошибкой, тело получает
эту ошибку даже после принятой отмены Job. Итоговый исход Job при этом
остаётся Cancelled. Проверка отмены после join относится к успешным
результатам операции.
Остановка самой операции
Заголовок раздела «Остановка самой операции»Для плеера, API которого принимает токен отмены:
Job<void> seek(Duration position) => run<Ready, void>( key: 'seek', policy: Policy.restart, (ctx) async { final token = CancelToken(); ctx.onCancel(token.cancel); // Дождаться остановки устройства до следующей перемотки. await ctx.join(() => _player.seek(position, cancelToken: token)); ctx.emit(ctx.state.copyWith(position: position)); }, );ctx.onCancel(callback) связывает отмену Job с механизмом отмены самой
операции; колбэк вызывается синхронно при пометке Job отменённой. Токен
запрашивает остановку перемотки, а join ждёт завершения этого запроса,
поэтому новая перемотка начинается только после него. Это зависит
от того, действительно ли API плеера реагирует на токен.
ctx.onCancel возвращает функцию удаления колбэка. Он передаёт операции
сигнал отмены, а параметр onCancel у run вычисляет итоговое состояние
контроллера после освобождения ресурсов Job.
Защита шага или всей задачи
Заголовок раздела «Защита шага или всей задачи»// Шаг, который нужно завершить после начала.SoloJob<void> commit(String entry) => run<Ready, void>((ctx) async { await ctx.uncancellable(() async { await payment.commit(); await journal.write(entry); }); });
// Job, отклоняющая все запросы, которые ей позволено отклонить.SoloJob<void> flush() => run<Ready, void>( cancellable: false, (ctx) => device.flush(), );Ручная отмена, отмена родителя и закрытие удерживаются, пока выполняется
действие uncancellable. Эти запросы пока не помечают Job отменённой,
поэтому откладываются и колбэки отмены, и передача отмены детям.
При завершении внешней секции удержанный запрос применяется. Следующая
контрольная точка бросает Cancelled; обычный код сразу после вызова
ещё может выполниться. Поместите всю обязательную работу внутрь секции
и всегда ожидайте её. Неожидаемая секция может пережить Job и потерять
удержанный запрос. Секции могут быть вложенными.
cancellable: false у Job полностью отклоняет такие запросы.
Удаление из очереди обычно сохраняет эти Job, но force: true
и close() могут отбросить их до старта. Работающую неотменяемую
Job close() дожидается.
Ни один механизм не отключает правила состояния. Job, которой
перестали соответствовать W или keepWhile, всё равно отменяется.
Job, работающей в любом состоянии, нужен базовый тип S
без ограничения keepWhile.
Обычный await и время жизни контекста
Заголовок раздела «Обычный await и время жизни контекста»SoloJob<void> upload(List<int> chunks) => run<Ready, void>((ctx) async { ctx.onDispose(() async { // Уборка ждёт несмотря на отмену, поэтому обычный await здесь // уместен: метод с учётом отмены отклонил бы эту Job. await device.flush(); }); for (final chunk in chunks) { // Между шагами оборачивать нечего, поэтому контрольная точка // стоит сама по себе. Обычный await здесь никому не ответил бы. ctx.check(); await ctx.join(() => device.write(chunk)); } });Обычный await не реагирует на отмену Job и может задержать её
завершение и close() на неопределённое время. Он уместен, когда нужно
намеренно дождаться операции несмотря на отмену, в том числе при
освобождении ресурсов или внутри секции uncancellable. Методы ожидания
с учётом отмены отклоняют уже отменённую Job, поэтому не могут выполнить
её уборку.
Не сохраняйте контекст для запуска работы после конца Job.
Методы emit, run, each, wait, join и uncancellable тогда
бросят StateError. Чтения и check остаются доступны после обычного
завершения; после отмены они продолжают бросать Cancelled. Во время
зарегистрированной уборки чтения состояния и операции тела недоступны.
Захватите необходимые для освобождения ресурсы замыканием. Во время
уборки остаются доступны log, job, регистрация уборки, disown
и unattended. Сам log не бросает исключение из-за отмены или
завершения Job.
Сведения об отмене
Заголовок раздела «Сведения об отмене»Собственная причина проносит через отмену данные приложения:
final class OutOfRange extends CancelReason { final Duration position;
const OutOfRange(this.position);
@override String get name => 'out of range';}
// Снаружи Job:await job.cancel(reason: OutOfRange(position));
// Внутри её тела:throw Cancelled.by(reason: OutOfRange(position), started: true);
// И на выходе:switch (job.outcome) { case Cancelled(:final reason, :final started): print('cancelled by ${reason.name}, started: $started'); case _:}Cancelled содержит reason, started, необязательный description
и стек вызовов отмены. started: false означает, что тело не запускалось.
Причины наследуют CancelReason. Встроенные типы включают
ManualCancelReason, ParentCancelReason, HandlerCancelReason,
ChainCancelReason, RulesCancelReason и ClosedCancelReason.
Проверяйте тип; name служит подписью, а не ключом для сравнения.
При передаче отмены между Job исходная отмена сохраняется в поле cause
причины.
job.whenCancelled(callback) регистрирует синхронный слушатель
и возвращает функцию его удаления. Слушатель вызывается, когда работающая
Job принимает отмену или Job отбрасывается до запуска. Если тело
отменяет себя, слушатель вызывается после завершения тела и детей,
перед освобождением ресурсов. Регистрация после отмены вызывает слушатель
сразу. Успешные и провалившиеся Job удаляют слушатели без вызова.
Асинхронный колбэк не ожидается; ошибки колбэка обрабатываются так же,
как ошибки ctx.onCancel.
Отмена работы и закрытие контроллера
Заголовок раздела «Отмена работы и закрытие контроллера»// Очистить очередь и остановить работающую Job; контроллер открыт.await controller.cancelAll();
// То же самое, и после этого новое не принимается.await controller.close();
// Либо сначала выполнить то, что уже в очереди, и закрыться после.await controller.close(mode: SoloCloseMode.drain);cancelAll() удаляет отменяемые Job из очереди, запрашивает отмену
работающей Job и ждёт её завершения. Контроллер продолжает принимать
работу. cancelAll(force: true) удаляет из очереди и неотменяемые Job;
force не влияет на то, принимает ли отмену работающая Job.
close() прекращает приём работы, отменяет все Job в очереди
с Cancelled(closed) и запрашивает отмену работающей Job. Он ждёт
её завершения, включая детей и освобождение ресурсов, даже если отмена
отклонена. Повторные вызовы возвращают ту же future. Новые попытки
добавления возвращают уже отменённые Job без исключения, поэтому
вызывающему коду не нужна проверка isClosed перед добавлением.
SoloCloseMode.drain закрывает контроллер, выполняя очередь, а не
сбрасывая её. С момента вызова новые корневые Job не принимаются,
а стоящие в очереди выполняются по обычным правилам: по порядку, со
своими детьми, своей уборкой и с выдержанным окном накопления, если оно
есть. Обычный close() поверх идущей досылки останавливает её на месте,
и тот же future, который все держат, завершается после этого. Выполнить
очередь — не обещание доставки: досылаемая Job всё ещё может упасть или
быть отклонена своими правилами, а буфер, хранящий события до
подтверждения отправки, строится поверх этого, а не внутри.
Закрытие само по себе не освобождает ресурсы приложения и не выбирает
итоговое состояние приложения. Оформите эту работу методом контроллера
и дождитесь её до close(), как в примере камеры.
Обработчик состояния отменённой Job ещё может обновить состояние
при закрытии.
Не ждите close() или cancelAll() внутри тела или уборки текущей
Job: каждый из них будет ждать ту самую Job, которая его вызвала.
Тело может закончиться через return или отменить себя через
throw Cancelled('reason').