Отмена, параллелизм и операции
Кооперативная отмена в долгих циклах
Синхронный цикл в обработчике блокирует поток воркера: он не получит сообщение об отмене, пока цикл не закончится. run() на главной стороне отклоняется сразу, но процессор остаётся занятым.
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()— недоступно: одна вкладка не может убить воркер, которым пользуются другие.
Режимы параллелизма
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()отменяет текущий и очищает очередь.
Типизированные операции
// 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().
Ошибки с данными
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'копирует вложенные реактивные прокси перед отправкой. В devDataCloneErrorуказывает путь к первому прокси.
Пул: 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 |