首页
/ tesseract.js 深入解析:Worker 与 Scheduler 两种 OCR 任务执行模式

tesseract.js 深入解析:Worker 与 Scheduler 两种 OCR 任务执行模式

2026-09-05 14:33:35作者:宣利权Counsellor

本文围绕 tesseract.js 中运行文字识别任务的两种核心方式展开:直接使用单个 Worker 执行识别,以及通过 Scheduler 管理 Worker 池并行执行批量任务。读完本文,你将理解两种模式各自的适用场景、Scheduler 的队列调度机制(可对照源码验证),以及在生产级 Node.js 服务端中周期性重建 Worker/Scheduler 的原因与做法,并掌握可直接复制运行的完整示例代码。

tesseract.js 提供两种运行识别任务(recognition jobs)的方式:

  1. 直接使用 Worker:创建一个 worker,直接在其上调用 recognize
  2. 使用 Scheduler + 多个 Worker:语法更复杂,但对大批量任务,通过多 worker 并行处理可以显著提升性能。

各函数的详细文档可参考 API 文档

方式一:直接使用 Worker

以下片段演示了使用单个 worker 从图片中识别文字:

(async () => {
    const worker = await Tesseract.createWorker('eng');
    const { data: { text } } = await worker.recognize('https://tesseract.projectnaptha.com/img/eng_bw.png');
    console.log(text);
    await worker.terminate();
})();

为什么应该把创建 Worker 与识别分开

在实际使用中,createWorker 步骤应与 worker.recognize 步骤分离,这样做有两个直接收益:

  1. Worker 可以提前准备好——例如在页面加载时就创建 worker,而不是等用户上传待识别图片后再开始初始化(初始化包含加载 WASM 核心与语言数据,耗时明显);
  2. Worker 可以复用于多个识别任务——不必每识别一张图就重新创建 worker 并重新加载语言数据。

但要注意:所有识别完成后,务必调用 worker.terminate() 释放内存。

从源码看,createWorker 在创建时并不是“拿到即用”的:它会先 spawnWorker 生成一个子环境(浏览器中是 Web Worker,Node 中是子进程),然后按 loadInternal()loadLanguageInternal(langs)initializeInternal(langs, oem, config) 的顺序串行下发初始化任务(见 src/createWorker.js),只有这三个阶段全部完成后 createWorker 返回的 Promise 才 resolve。这正是“worker 可提前创建、复用多次”这一建议背后的成本来源——核心与语言数据的加载只在创建/初始化阶段发生一次。

此外,src/Tesseract.js 中还提供了一个一次性快捷方法 Tesseract.recognize(image, langs, options):它内部自动 createWorker、执行识别并在 finallyterminate。这种方式适合临时脚本,但把“创建 + 识别 + 销毁”绑死在了一起,无法体现上述复用收益,批量场景下应改用显式的 worker 或 scheduler 写法。

方式二:使用 Scheduler + Worker 池

tesseract.js 支持用 scheduler 执行任务。Scheduler 是一个内部包含多个 worker 的对象,它用这些 worker 并行执行任务。下面是一个“4 个 worker 并行执行 10 个识别任务”的完整示例:

const scheduler = Tesseract.createScheduler();

// 创建 worker 并加入 scheduler
const workerGen = async () => {
  const worker = await Tesseract.createWorker('eng');
  scheduler.addWorker(worker);
}

const workerN = 4;
(async () => {
  const resArr = Array(workerN);
  for (let i=0; i<workerN; i++) {
    resArr[i] = workerGen();
  }
  await Promise.all(resArr);
  /** 添加 10 个识别任务 */
  const results = await Promise.all(Array(10).fill(0).map(() => (
    scheduler.addJob('recognize', 'https://tesseract.projectnaptha.com/img/eng_bw.png').then((x) => console.log(x.data.text))
  )))
  await scheduler.terminate(); // 同时终止所有 worker
})();

单任务场景下 scheduler 并不比直接用 worker 更高效,但它可以快速并行执行大量任务。仓库中的 examples/node/scheduler.js 是该示例的 Node 可运行版本(注意其 createWorker('eng', 1, { cachePath: '.' }) 中显式指定了缓存路径),浏览器端则可见 examples/browser/basic-scheduler.html:它监听文件上传控件,对每个上传文件调用 scheduler.addJob('recognize', files[i]) 投递任务,注释中明确指出“单文件时性能相近,多文件时并行处理速度显著更快”。

Scheduler 的 API

API 文档中列出的 Scheduler 方法如下:

  • createScheduler(): Scheduler —— 工厂函数,创建一个管理任务队列和 worker 池的 scheduler;
  • scheduler.addWorker(worker): string —— 将 worker 加入 worker 池,建议一个 worker 只加入一个 scheduler,返回 worker 的 id;
  • scheduler.addJob(action, ...payload): Promise —— 把任务加入队列,scheduler 会等待并找到空闲 worker 来执行;
  • scheduler.getQueueLen(): number —— 返回当前任务队列长度;
  • scheduler.getNumWorkers(): number —— 返回已加入 scheduler 的 worker 数量;
  • scheduler.terminate(): Promise —— 终止所有已加入的 worker,用于快速清理。

源码级调度机制

createScheduler 的实现揭示了任务是如何被分配的,核心是三块状态:

  • workers:worker 池(以 worker.id 为键);
  • jobQueue:待执行任务数组,FIFO 顺序;
  • runningWorkers:正在执行的 worker 与其任务的映射。

调度逻辑在 dequeue() 中(src/createScheduler.js):只要队列非空,就按 workers 中的 key 顺序遍历,找到第一个“当前没有正在运行的任务”(runningWorkers[wIds[i]] 为 undefined)的 worker,让队首任务(jobQueue[0])执行并立即 break。每次 addWorker、任务完成(finally 中)都会再次触发 dequeue,从而保持队列尽可能被消费。

几个值得注意的实现细节:

  1. addJob 要求至少有一个 workeraddJob 内部检查 getNumWorkers() === 0 时直接抛出 You need to have at least one worker before adding jobssrc/createScheduler.js),即先加任务后加 worker 会立即报错,而不是排队等待;
  2. 任务与 jobId 的关联queue() 通过 createJob 创建 { id, action, payload } 结构,并以 job.id 作为传给 worker 动作的 jobId,例如 w['recognize'].apply(this, [...payload, job.id])——这使得 worker 内部可以按 action-jobId 唯一定位待 resolve 的 Promise,并在日志中追踪任务流转;
  3. terminate 的副作用scheduler.terminate() 会遍历 workers 逐个 worker.terminate(),并把 jobQueue 置空——即文档中“It also terminates all workers”的出处。

由于 dequeue 选择的是“第一个空闲 worker”而非某个指定 worker,任务与 worker 的绑定关系是非确定性的。这也是文档强调的一条重要约束:加入同一 scheduler 的 worker 应当是同质的(homogenous)——相同的语言、相同的参数配置。否则识别结果会取决于任务恰好被分配到了哪个 worker。这一点从 queue/dequeue 源码结构上可以印证:调度器不感知 worker 之间的差异,只做“有空闲就派活”这一件事。

仓库的测试 tests/scheduler.test.mjs 验证了并行正确性:预先创建 5 个 worker,然后分别只取 1、3、5 个加入新的 scheduler,每次向 scheduler 投递 10 个 recognize 任务并用 Promise.all 等待全部完成,断言返回结果数量与任务数一致(expect(rets.length).to.be(NUM_JOBS)),覆盖了 1 到多 worker 的边界情况。

在 Node.js 服务端代码中复用 Worker/Scheduler

Worker 和 Scheduler 都是可复用的,官方也建议在不同任务之间复用它们。但有一个重要的例外:在长时间运行的 Node.js 服务端代码中,同一个 worker/scheduler 连续使用一周会出问题。因此在服务端场景中,应当定期销毁并重建 worker/scheduler——例如每 500 个任务后 terminate 并重新创建一次 scheduler。

文档给出了两条具体的技术原因:

  1. WASM 内存只增不减:受 WebAssembly 通用限制,分配给 worker 的内存只能随时间增长。因此一张特别大的图片会永久性抬高该 worker 的内存占用,后续再处理小图片也不会回收;
  2. 内部词典会“学习”膨胀:worker 会随时间把任务中遇到的新词加入内部词典。这一行为在识别同一文档或同一组相关文档时是有益的,但在识别数百个互不相关的文档时未必是期望行为——如果一个 scheduler 一周跑几千个任务,内部词典最终会膨胀并混入大量拼写错误(typos)。

对应到源码,worker 的销毁路径是 worker.terminate(调用 terminateWorker 后把内部 worker 置为 null),而 scheduler 的销毁路径则是 scheduler.terminate(终止全部 worker 并清空队列)。服务端实践上,即是用这两个 terminate 加上重新 createScheduler/createWorker 的循环,实现文档建议的周期性“重置”。

小结:如何选择

  • 单个任务或低频识别:直接 createWorker + recognize 即可,语法最简;记得把 worker 创建放在等待用户输入之前,识别完成后 terminate
  • 批量图片、多文件并发(如浏览器端一次上传多张图片、服务端批量处理):用 createScheduler 建立 4 个左右的 worker 池,逐个 addJob 投递任务,最后 scheduler.terminate() 统一清理;
  • 长期运行的 Node.js 服务:在 scheduler 复用之上叠加“每 N 个任务重建一次”的运维策略,以规避 WASM 内存只增不减与内部词典膨胀两个问题。

相关文档与代码入口:docs/workers_vs_schedulers.mdAPI 文档src/createScheduler.jssrc/createWorker.jsexamples/node/scheduler.jstests/scheduler.test.mjs

登录后查看全文
热门项目推荐
相关项目推荐

项目优选

收起
kernelkernel
deepin linux kernel
C
33
18
ops-transformerops-transformer
本项目是CANN提供的transformer类大模型算子库,实现网络在NPU上加速计算。
C++
1.12 K
2.72 K
kernelkernel
openEuler内核是openEuler操作系统的核心,既是系统性能与稳定性的基石,也是连接处理器、设备与服务的桥梁。
C
528
588
ops-nnops-nn
本项目是CANN提供的神经网络类计算算子库,实现网络在NPU上加速计算。
C++
906
1.83 K
pytorchpytorch
作为 Ascend for PyTorch 社区的核心组件,TorchNPU 是昇腾专为 PyTorch 打造的深度学习适配插件,使 PyTorch 框架能够直接调用昇腾 NPU,为开发者提供昇腾 AI 处理器的超强算力。
Python
854
1.34 K
docsdocs
暂无描述
Markdown
891
5.78 K
jiuwenswarmjiuwenswarm
JiuwenSwarm 是一款基于openJiuwen开发的智能AI Agent,它能够将大语言模型的强大能力,通过你日常使用的各类通讯应用,直接延伸至你的指尖。
Python
3.53 K
1.01 K
ops-mathops-math
本项目是CANN提供的数学类基础计算算子库,实现网络在NPU上加速计算。
C++
1.34 K
1.45 K
cann-learning-hubcann-learning-hub
CANN 学习中心仓,支持在线互动运行、边学边练,提供教程、示例与优化方案,一站式助力昇腾开发者快速上手。
Jupyter Notebook
987
506
AscendNPU-IRAscendNPU-IR
AscendNPU-IR是基于MLIR(Multi-Level Intermediate Representation)构建的,面向昇腾亲和算子编译时使用的中间表示,提供昇腾完备表达能力,通过编译优化提升昇腾AI处理器计算效率,支持通过生态框架使能昇腾AI处理器与深度调优
C++
540
384