Skip to content

Отмена, параллелизм и операции ​

Кооперативная отмена в долгих циклах ​

Синхронный цикл в обработчике блокирует поток воркера: он не получит сообщение об отмене, пока цикл не закончится. run() на главной стороне отклоняется сразу, но процессор остаётся занятым.

ts
import { defineWorkerHandler } from 'vue-worker-kit/worker'

export default defineWorkerHandler(async (rows: Row[], ctx) => {
  const out = []
  for (let i = 0; i < rows.length; i++) {
    out.push(heavy(rows[i]))
    if (i % 1000 === 0) await ctx.yield()
  }
  return out
})
  • await ctx.yield() — отдаёт управление циклу событий, чтобы отмена могла прийти, и затем бросает ошибку, если запуск отменён.
  • ctx.throwIfAborted() — только проверка, без передачи управления.

WARNING

Пул без hardCancelOnAbort отменяет выполняющиеся задачи только кооперативно. Задача в очереди отклоняется сразу; выполняющаяся держит свой слот, пока обработчик не вызовет yield или не завершится.

Как часто вызывать yield() ​

Вызывайте его раз в несколько миллисекунд работы, а не на каждой итерации. Замеры в Chromium, Firefox и WebKit на стенде cancel-yield.html из демо: при примерно 4 мс работы между вызовами yield() цикл останавливается не позже чем через два следующих вызова yield() в 100 из 100 попыток во всех трёх браузерах. Один yield() стоит около 20 мкс в Chromium и Firefox и 0,3–0,4 мс в WebKit.

Сообщению об отмене нужно время, чтобы дойти из основного потока в воркер. Если отдавать управление гораздо чаще этого времени (каждые ~0,3 мс), растёт лишь число вызовов: в Firefox до остановки цикла встречалось до восьми вызовов yield(), хотя остановка всё равно происходила за миллисекунды. Хорошее правило — 1–5 мс работы между отдачами управления.

Жёсткая отмена ​

hardCancelOnAbort: true завершает поток воркера, не дожидаясь обработчика.

  • Варианты:
    • useWorker() — опция экземпляра. При отмене воркер завершается, а следующий run() создаёт новый.
    • Пул — опция пула, которую RunOptions.hardCancelOnAbort / WorkerMapOptions.hardCancelOnAbort могут переопределить для одного вызова (приоритет у вызова). Завершается только слот отменённой задачи; map() с общим signal завершает все занятые слоты.
    • useSharedWorker() — недоступно: одна вкладка не может убить воркер, которым пользуются другие.

Режимы параллелизма ​

ts
const search = useWorker<typeof import('./search.worker')>(factory, {
  concurrency: 'latest',
  hardCancelOnAbort: true,
})
  • Варианты:
    • 'parallel' (по умолчанию) — каждый run() стартует сразу.
    • 'latest' — новый запуск вытесняет предыдущие. Они отклоняются с AbortError, у которой reason равен WORKER_SUPERSEDED; проверяйте через isSupersededError(). Эта ошибка не попадает в error, а собственный signal вызывающего не отменяется. С hardCancelOnAbort занятый воркер завершается и создаётся заново.
    • 'serial' — запуски идут по одному; cancel() отменяет текущий и очищает очередь.

Типизированные операции ​

ts
// tools.worker.ts
export default defineWorkerOperations({
  parse: (input: { text: string }) => ({ length: input.text.length }),
  format: async (input: { value: number }) => `v=${input.value}`,
})

// main
const tools = useWorker<typeof import('./tools.worker')>(factory)
const parsed = await tools.call('parse', { text: 'hello' })

Имена, входы и выходы выводятся автоматически. Работает с useWorker, пулами и useSharedWorker. Неизвестное имя даёт ошибку с code: 'UNKNOWN_OPERATION'. Ключ кэша включает имя операции. У модуля, созданного через defineWorkerOperations(), есть call() и нет run().

Ошибки с данными ​

ts
throw new WorkerHandlerError('Bad row', { code: 'E_ROW', details: { row: 7 } })

code и details приходят в WorkerError вместе с цепочкой workerCause (до трёх уровней). Если details нельзя клонировать, оно заменяется строкой, а detailsDropped становится true.

Колбэки вызова и unwrap ​

  • run(input, { onProgress, onChunk }) — колбэки для одного вызова, помимо общих ref.
  • unwrap: 'shallow' | 'deep' — 'deep' копирует вложенные реактивные прокси перед отправкой. В dev DataCloneError указывает путь к первому прокси.

Пул: terminate, restart, простаивающие слоты и affinity ​

  • pool.terminate() закрывает пул. Ожидающие и выполняющиеся задачи, а также любые последующие run()/call()/map() отклоняются с WorkerError, у которой code равен 'POOL_TERMINATED' (не AbortError). pool.restart() открывает пул снова. У одиночного воркера terminate() иной: он лишь освобождает воркер, а следующий run() создаёт новый.
  • idleTimeout — завершает простаивающие слоты.
  • RunOptions.affinity и affinityMode: 'prefer' | 'strict' у пула удерживают задачи с одним ключом на одном слоте; правила описаны на странице Пул воркеров.

Что выбрать ​

НужноИспользовать
Одна тяжёлая задача за раз, реактивное состояниеuseWorker
Живой ввод, важен только новейший результатuseWorker с concurrency: 'latest' и hardCancelOnAbort
Много независимых мелких задач на ядрахcreateWorkerPool
Значение, производное от реактивного состоянияuseWorkerComputed
Одно вычисление на все вкладкиuseSharedWorker