Перейти к содержимому

async_job

async_job добавляет к асинхронному коду на Dart отмену, дочерние задачи и освобождение ресурсов. Пакет работает на чистом Dart, из зависимостей только meta.

Job<T> выполняет функцию, которая называется телом задачи. Через await job.done можно получить исход работы, а через await job.value значение, возвращённое телом. Сам Job не реализует Future.

У обычной Future нет отмены. Флаг может попросить работу остановиться, но за то, что она уже открыла, не отвечает никто:

var cancelled = false;
Future<void> load() async {
final db = await Database.open();
if (cancelled) {
return; // база остаётся открытой
}
final rows = await db.readAll();
if (cancelled) {
return; // и здесь тоже
}
use(rows);
}

Job отвечает:

final job = Job<void>((ctx) async {
// Проверка перед вызовом и ещё раз перед возвратом значения,
// а база закрывается, чем бы Job ни кончилась.
final db = await ctx.join(Database.open, dispose: (db) => db.close());
use(await ctx.join(db.readAll));
});
// Ждёт тело, его детей и его уборку.
await job.cancel();
print(job.outcome); // Cancelled(manual)

Job управляет этим жизненным циклом. Он завершается одним из трёх исходов: Done, Failed или Cancelled с причиной. Перед завершением он ждёт дочерние задачи и выполняет зарегистрированную уборку. Если исход Failed никто не наблюдал, ошибка передаётся в зону создания задачи, как необработанная ошибка Future в Dart. Для ошибок работы, которую тело уже не ждёт, Job также предоставляет наблюдателя.

Отмена кооперативная: cancel() запрашивает её, а тело останавливается на контрольной точке. Job предоставляет эти точки через контекст ctx, переданный телу. ctx.join(action) проверяет отмену перед запуском операции, дожидается её завершения и проверяет отмену ещё раз, прежде чем вернуть значение телу.

Прямой await action() допустим, но отмену Job не проверяет. Он продолжает ждать, а код после него может выполниться, даже если задача уже отменена. Поэтому использование контекста входит в правила написания отменяемого тела. CancelableOperation отменяет ожидание; Job владеет тем, что работа оставила после себя.

В async_job нет управления состоянием, очереди и правил планирования. Если они нужны, используйте solo: он добавляет эти возможности поверх async_job и реэкспортирует его API.

Повторы, таймауты и пул задач не входят ни в один из пакетов; их можно добавить в приложении.

Окно терминала
dart pub add async_job

Допустим, нужно открыть базу, выполнить миграцию и вернуть открытую базу вызывающему коду. При ошибке или отмене базу нужно закрыть. Контекст позволяет описать оба варианта в теле:

import 'package:async_job/async_job.dart';
final job = Job<Database>((ctx) async {
final database = await ctx.join(
Database.open,
discard: (database) => database.close(),
);
final stop = CancelToken();
ctx.onCancel(stop.cancel);
await ctx.join(() => database.migrate(stop));
await ctx.uncancellable(() => database.markReady(stop));
return database;
});
// Кто-то передумал, пока база открывалась.
await Future<void>.delayed(const Duration(milliseconds: 10));
await job.cancel();
final outcome = await job.done; // Cancelled(manual)
  • Тело запускается на следующей микротаске. Вызывающий код сначала получает задачу и может зарегистрировать обработчики до её запуска. Если отменить задачу сразу после создания, тело не выполнится, а исходом станет Cancelled(manual) с started: false. Задержка в примере даёт телу запуститься.
  • ctx.join(Database.open) вызывает Database.open и возвращает открытую базу. Если во время открытия придёт отмена, метод дождётся завершения вызова и бросит Cancelled, если открытие прошло успешно. Если открытие завершится ошибкой, он передаст её без изменений, даже после отмены.
  • discard: (database) => database.close() закрывает базу, если задача завершается отменой или ошибкой. При Done(database) база остаётся открытой для вызывающего кода. Уборка учитывает и отмену после return database: например, если тело запустило дочернюю задачу, которая ещё работает, перед завершением задача будет её ждать. Отмена во время этого ожидания приведёт к исходу Cancelled и закрытию базы. См. Уборку.
  • ctx.onCancel(stop.cancel) связывает отмену задачи с токеном отмены базы. Колбэк вызывается сразу после принятия отмены, до следующей контрольной точки в теле.
  • ctx.join(() => database.migrate(stop)) ждёт завершения миграции. При отмене токен просит миграцию остановиться, а join ждёт её остановки, прежде чем задача закроет базу. Если нужно прекратить ожидание сразу при отмене, используйте ctx.wait. Он прекращает ожидание, но не останавливает саму операцию.
  • ctx.uncancellable(() => database.markReady(stop)) защищает этот заключительный шаг от сигнала остановки. При миграции join ждёт, пока база останавливается по токену. Здесь шаг должен завершиться, не получая такого сигнала, поэтому uncancellable откладывает запрос отмены: onCancel не срабатывает и токен остаётся активным во время вызова. После секции запрос вступает в силу, а следующая контрольная точка контекста бросает Cancelled. См. Отмену.
  • await job.cancel() запрашивает отмену и ждёт завершения задачи. Это ожидание включает уборку, поэтому исход на следующей строке уже готов. Если достаточно только запросить отмену, await можно опустить.

Полный запускаемый пример с имитацией Database находится в example/example.dart.

Они же на сайте документации, с поиском.

Страница О чём
Исходы Done, Failed, Cancelled и кто отвечает за ошибку
Отмена Контрольные точки, onCancel, uncancellable, причины
Дети, стримы и цепочки ctx.run, ctx.each, then
Уборка dispose, discard, onDispose, порядок
Наблюдение и тестирование JobObserver, логи, фейковое время
Своё поверх ядра JobBase, отложенный старт, свой движок

solo добавляет состояние, очередь и декларативные правила. Он выполняет одну корневую задачу за раз и даёт ей исключительный доступ к состоянию. Используйте его, если нужен контроллер с такими гарантиями. Он реэкспортирует async_job, поэтому достаточно зависимости от solo.

Задачи работают везде, где работает Dart. Для интеграции solo с виджетами используйте flutter_solo.