Задачи и очередь
Задачи и результаты
Заголовок раздела «Задачи и результаты»Job<T> представляет одну операцию и её будущий исход. Он не является
Future: вызов profile.load() без await допустим, а что делать
с результатом, решает место вызова.
switch (await profile.load().done) { case Done(:final value): print('loaded $value'); case Failed(:final error): print('not loaded: $error'); case Cancelled(:final reason): print('gave up: $reason');}У Outcome<T> эти три варианта: Done содержит значение, Failed —
ошибку и стек вызовов, Cancelled — причину отмены.
| Член API | Назначение |
|---|---|
job.value |
Дождаться T; бросает исключение при ошибке или отмене. |
job.done |
Дождаться Outcome<T> без исключения. |
job.outcome |
Прочитать исход синхронно после завершения. |
job.cancel() |
Запросить отмену и дождаться завершения. |
job.ignore() |
Пометить исход обработанным без ожидания. |
Ошибка Job не останавливает очередь. Ошибка передаётся обработчикам, после чего может выполняться следующая Job. Отмена представляет отдельный исход: закрытие контроллера, удаление Job из очереди или нарушение правил состояния могут отменить работу без ошибки приложения.
Каждая ошибка передаётся хуку ошибок контроллера и наблюдателю. Если
к исходу провалившейся Job никто не обратился через done, value
или ignore(), ошибка также передаётся как необработанная в зону Dart,
где Job была создана. Используйте ignore(), когда достаточно
обработки ошибок в другом месте; отсутствие await само по себе
не помечает ошибку обработанной. Подробнее в разделе
Сообщения об ошибках и наблюдение.
Создание и запуск задач
Заголовок раздела «Создание и запуск задач»// Сначала собрать, поставить потом: два шага, если Job нужно придержать.final saving = job<Ready, void>(key: _Op.save, (ctx) => ctx.join(store.save));add(saving, policy: Policy.droppable);
// Либо всё сразу — так обычно и делает метод контроллера.SoloJob<void> setZoom(double zoom) => run<Ready, void>( key: _Op.zoom, // Лениво и только для диагностики: строится, когда спросит лог. describe: () => 'zoom: $zoom', (ctx) => ctx.join(() => camera.zoom(zoom)), );Все три возвращают SoloJob<T>, который реализует Job<T> и добавляет
isQueued. Обычно контроллер предоставляет предметные методы вроде
load() или setZoom(), чтобы вызывающему коду не приходилось собирать
Job самостоятельно.
key — это то, по чему политики очереди ищут родственную работу,
а describe задаёт подпись для логов, наблюдателей и toString():
Job(key: label). Без описания используется представление Job(key).
Очередь и политики
Заголовок раздела «Очередь и политики»Job, поставленная в очередь контроллера, является корневой. В каждый момент работает одна. Какая из них переживёт повторный вызов, решает политика:
enum _Op { load, save, zoom, seek, stop }
// sequential, по умолчанию: одна за другой, в порядке обращения.SoloJob<void> save() => run<Ready, void>(key: _Op.save, (ctx) => ctx.join(store.save));
// droppable: повторная загрузка того же профиля вернёт первую Job.SoloJob<Profile> load(String id) => run<Loaded, Profile>( key: (_Op.load, id), policy: Policy.droppable, (ctx) async => ctx.wait(() => api.load(id)), );
// replace: ожидающий зум уходит, работающий остаётся нетронутым.SoloJob<void> setZoom(double zoom) => run<Ready, void>( key: _Op.zoom, policy: Policy.replace, (ctx) => ctx.join(() => camera.zoom(zoom)), );
// restart: то же самое, и работающую просят остановиться.SoloJob<void> seek(Duration position) => run<Ready, void>( key: _Op.seek, policy: Policy.restart, (ctx) => ctx.join(() => camera.seek(position)), );| Политика | Когда Job с таким ключом уже существует |
|---|---|
Policy.sequential |
Добавить новую Job в очередь. |
Policy.droppable |
Вернуть существующую Job из очереди или работающую; новую отбросить. |
Policy.replace |
Удалить отменяемые Job с этим ключом из очереди, затем добавить новую. |
Policy.restart |
Сделать то же, что replace, и запросить отмену работающей Job с этим ключом. |
Корневая Job удерживает очередь до завершения тела, детей, освобождения ресурсов и итогового обработчика состояния. Дочерние Job могут работать в это время; они разобраны в Дочерние задачи и стримы.
restart запрашивает отмену при добавлении новой Job. Новая Job всё
равно ждёт завершения текущей; их тела не пересекаются. Поэтому Job,
отклоняющая отмену, может задержать свою замену.
Ключом может быть любой объект со сравнением через ==, поэтому запись
(_Op.load, id) выше даёт политике идентичность одного запроса, а не
операции вообще: droppable отбрасывает вторую загрузку того же профиля
и пропускает загрузку другого, тогда как голый _Op.load отбросил бы
обе.
Используйте разные ключи для разных операций и типов результата.
Повторное использование ключа для Job<String> и Job<void> заставит
droppable бросить ArgumentError до того, как новая Job будет
затронута, поэтому её можно добавить снова под собственным ключом. Enum
с ключом на метод — удобный способ этого избежать. Политика, отличная
от последовательной, с ключом null тоже бросает ArgumentError,
и обе проверки работают в release-сборках.
Сама очередь принадлежит контроллеру, и метод может работать с ней напрямую:
SoloJob<void> stop() { // Что ждёт прямо сейчас. print(queue.jobs.length); // Убрать то, что эта команда обесценила... queue.removeWhere((job) => job.key == _Op.zoom); // ...и пропустить саму остановку без очереди. return add( job<Ready, void>(key: _Op.stop, (ctx) => ctx.join(camera.stop)), first: true, );}queue предоставляет jobs, remove, removeWhere, clear
и lastWhere. Методы удаления касаются только Job в очереди — работающая
им не принадлежит — и сохраняют Job с cancellable: false, если
не вызваны с force: true. Группы накопителя, ждущие своего окна, могут
пропускать готовые Job; см. Накопление событий.