不知道大家接触过 Web Worker 相关的内容吗,今天咱们深入的盘一盘这块内容,希望对你有所帮助。
作用
Web Worker本质上就是新开辟了一条线程,大部分情况下创建 Web Worker 的目的是与主线程(UI线程)区分开。
主线程(UI 线程)负责DOM渲染、页面交互、无特殊处理的Js运算...
但是当海量计算出现在主线程,就会导致页面卡顿。
这时候就可以开辟一条新的线程,将主要计算放在新线程中进行。
这就是 Web Worker 的核心作用。
简单的 Demo 创建一个 Worker。
const worker = new Worker('./calc.worker.js')
worker.onmessage = (e) => {
console.log('worker结果:', e.data)
}
worker.postMessage({num: 10})
新创建一个名为 calc.worker.js 的文件:
self.onmessage = (e) => {
let sum = 0;
for (let i=0; i<e.data.num; i++) {
sum += i;
}
self.postMessage(sum)
}
Worker中存在好几种分类,这次咱们只重点说 Dedicated Worker,也就是专用Worker。
除此之外还有Shared Worker、Service Worker、AudioWorklet、CSS Painting Worklet等(这里挖个坑,回头一点点补全)。
区别
Worker创建的线程和真正的操作系统线程是存在区别的:
- Worker(专用Worker)不能共享 Js 对象内存,没有Java那种共享内存的读写操作。(
Shared Worker另说) - Worker线程仅用于计算,不能操作DOM,不承接页面交互。
- Worker本身也受浏览器事件循环的影响,只是Worker本身也有自己的事件循环。 你可以简单理解为浏览器事件循环是大圈,Worker事件循环是其中的小圈。
- Worker是有上限的(这一点后面详细说)
从上面的例子中,你应该能够感觉的出来,Worker的运算是异步的。
主线程通过 postMessage 向Worker发送任务,通过 onmessage 回调接收Worker的回传。
发送任务和接收结果本身都不是同步的,它们都受到计算量、浏览器线程调度、任务排队等的影响。
对于 专用Worker 而言,它绑定的是当前页面的上下文,也只有当前页面可以和这个Worker通信。
如果这个页面关闭,则这个Worker也会直接被销毁。
或者你也可以调用 worker.terminate() 方法手动销毁。
拆分
那么,智商占领了高地的你很快就会想到:
我是不是完全可以将所有的计算任务都放在Worker里?这样我不就让主线程完全避开计算损耗,只专注UI渲染。
对此,我的答案是:可以!但结果并不一定是你想要的。
首先,所有的计算放在 Worker 中,在设计上来说是可行的。
但是,问题出现在创建 Worker 上。
Worker 实例的创建本身有冷启动性能开销,这部分开销是不可避免的。
当代码创建一个新的 Worker 时,它需要先通过浏览器操作系统线程,新建一个独立的 V8 Isolate(独立堆、独立 JS 环境)。
然后加载、解析、编译 worker.js 脚本。
整个一套下来可能需要几十毫秒,甚至是几百毫秒。
其次,开销还表现在通信上。
通过 postMessage 进行通信的时候也存在开销,即便是小对象,也有几个毫秒的开销。
这个开销还不是一次性的,而是你每次通信他都会出现。
所以,你可以将计算任务都放在Worker中,但是我不建议!
不是说计算在 Worker 中就更快,Worker 的核心价值是计算不发生在主线程,避免了阻塞UI渲染。
普通计算主线程因为没有 创建 & 通信 的性能开销,反而优于 Worker。
并行
智商又一次占领高地的你肯定又很快想到:
Worker既然能够将计算"分流",那么我创建多个Worker是不是就能细分计算,将大计算拆解成一块块的小计算,提高计算速度?
对此,我的答案是:可以!但适度使用。
前端本身是能够创建多个 Worker 分流计算的,每次 new Worker 就创建一个子线程,理论上来说这就是并行计算。
但要注意,浏览器创建 Woker 是有上限的,Chrome 一般最大也就是 8 个。
而且 Worker 数量还收本地设备的限制,如果你的电脑是 4核心8线程 的CPU,理论上来说,可以最多开 8 个 Worker 同时计算。
但如果你同时创建了 20 个Worker,并不是20个同时跑。
大部分 Worker 只是挂起等待消息,一旦同时全部塞 CPU,就会触发 OS 线程调度排队,性能暴跌。
每一个 Worker 自带独立 V8 Isolate,内存开销不小,大量 Worker 会吃非常多的内存。
这里注意,我前面说"理论上"可以创建8个Worker,但其实不行!
因为操作系统本身要占去一部分CPU的资源,其他任务也要占,浏览器的其他任务也要占,所以真正给到你的,可用的Worker数量并不会太多。
我们可以使用 navigator.hardwareConcurrency API拿到机器逻辑上的核心数,所以我们更推荐大家创建:
Math.max (2, navigator.hardwareConcurrency ‑ 1) 个Worker数量。
到这里,我相信你大概已经知道了 Worker 的优势和边界。
挖坑
但是你有没有发现,我一直在说 CPU 的事儿。
但是现在主流的计算不是都已经发生在了 GPU 中吗?
比如AI训练/推理、图形渲染等等
那么 Worker 的计算为什么不放在 GPU 做,从而绕开 CPU 的性能瓶颈?
这部分咱们下一篇详细分析一下,感兴趣的可以点一下"关注",我将非常感谢!