本文最后更新于:2021年10月11日 凌晨
场景
由于需要做一些 CPU 密集型的计算,为了优化性能,吾辈开始尝试使用 worker 将计算任务放到其它线程(主要还是为了避免主线程卡死)。
主要场景包括
- 浏览器上的
WebWorker/SharedWorker
:处理音频数据
- nodejs 中的
worker_threads
:解析 md/ts ast 然后处理
为什么不用 wasm?– 主要是由于它需要从零开始编写相关的代码,而非可以直接对现有的 js 代码稍微修改便能提高性能。
浏览器
目前 vite 对此默认支持浏览器 worker,不再需要任何配置,仅使用 comlink 简化使用体验即可。
1 2 3 4 5 6 7 8
| import { expose } from 'comlink'
export function hello(name: string) { return `hello ${name}` }
expose(hello)
|
1 2 3 4 5 6 7 8 9
| import type { hello } from './hello.worker' import HelloWorker from './hello.worker?worker' import { wrap } from 'comlink' ;(async () => { const asyncHello = wrap<typeof hello>(new HelloWorker()) const res = await asyncHello('liuli') console.log(res) })()
|
nodejs
nodejs 本身提供的解决方案
nodejs 仍然像浏览器一样基于文件系统使用 worker_threads,但官方确实提供了一种有趣的解决方案 – 在同一个文件中包含主线程与 worker 线程的代码,然后在其中使用逻辑判断。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30
| import { isMainThread, parentPort, Worker } from 'worker_threads' import { expose, wrap } from 'comlink' import nodeEndpoint from 'comlink/dist/umd/node-adapter' import { wait } from '@liuli-util/async' import { worker as helloType } from './worker'
async function _hello(name: string) { await wait(100) return `hello ${name}` }
let hello: (name: string) => Promise<string>
if (isMainThread) { hello = async (name: string) => { const worker = new Worker(__filename) try { const helloWorker = wrap<typeof helloType>(nodeEndpoint(worker)) return await helloWorker(name) } finally { worker.unref() } } } else { hello = _hello expose(_hello, nodeEndpoint(parentPort!)) }
export { hello }
|
1 2 3 4 5
| import { hello } from './worker.js' ;(async () => { console.log(await mixingThreadHello('liuli')) })()
|
看起来很简洁,但这种方案也并非尽善尽美,吾辈实际使用时发现以下问题
参考:nodejs Worker
使用打包工具处理的调研方案
- rollup-plugin-web-worker-loader
- 默认不会处理 ts 文件
- worker 中包含依赖时不会打包
- rollup-plugin-worker-factory
- @surma/rollup-plugin-off-main-thread
目前没有找到满意的插件,后续可能不得不写一个 rollup 插件。还是那句话,没有对比就没有伤害,如果没有 vite 对 ts+worker 的良好支持,或许吾辈还能忍受这种糟糕的开发体验。
尝试编写 rollup 插件 rollup-plugin-worker-threads,以在 nodejs 中对 worker_threads
提供类似 vite 的开发体验。
大致使用方式如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30
| import { expose, Remote, wrap } from 'comlink' import path from 'path' import { isMainThread, parentPort, Worker } from 'worker_threads' import nodeEndpoint from 'comlink/dist/umd/node-adapter'
export function wrapWorkerFunc<T extends (...args: any[]) => any>( ep: T, ): Remote<T> { if (path.extname(__filename) !== '.js') { return ep as Remote<T> } if (isMainThread) { return ((...args: any[]) => { const worker = new Worker(__filename) const fn = wrap<T>(nodeEndpoint(worker)) return (fn(...args) as Promise<any>).finally(() => worker.unref()) }) as Remote<T> } expose(ep, nodeEndpoint(parentPort!)) return ep as Remote<T> }
|
1 2 3 4 5 6 7 8 9 10
| import { wait } from '@liuli-util/async' import { wrapWorkerFunc } from './util/wrapWorkerFunc'
async function _hello(name: string) { await wait(100) return `hello ${name}` }
export const hello = wrapWorkerFunc(_hello)
|
1 2
| export * from './hello.worker'
|
参考