Javascript 多线程编程的前世今生
myzbx 2025-05-09 20:34 28 浏览
作者: jolamjiang 腾讯技术工程
转发链接:
https://mp.weixin.qq.com/s/87C9GAFb0Y_i5iPbIL5Hzg
为什么要多线程编程
大家看到文章的标题《Javascript 多线程编程》可能立马会产生疑问:Javascript 不是单线程的吗?Javascript IO 阻塞和其他异步的需求(例如 setTimeout, Promise, requestAnimationFrame, queueMicrotask 等)不是通过事件循环(Event Loop)来解决的吗?
没有错,Javascript 的确是单线程的,阻塞和其他异步的需求的确是通过实现循环来解决的,但是这套机制当线程需要处理大规模的计算的时候就不大适用了,试想一下一下的场景:
- 你需要实现对文件的加解密。
- 你的 VirtualDom 树有很多元素(例如上万棵),你需要对这棵树进行 Diff 操作。
- 你需要在浏览器“挖矿”。
上面这些场景都会阻塞主线程,也就是当进行这些操作的时候,你的页面是卡住的,设置当页面卡住一段时间之后,Chrome 等浏览器或者操作系统会建议你 Kill 掉整个 Tab 或者进程。这显然不是我们想看到的事情。正因为这些场景的存在,浏览器提出了 W3C 在 2013 年提出了 Web Worker 草案,这个草案的提出就是为了解决上述这些问题。
为了让大家感受 JS 多线程能够干什么,笔者写了一个基于 Web Worker(线程)、ShareArrayBuffer(共享内存)、Atomics(锁)等 Web API 的在前端压缩和解压文件(基于 DEFLATE 算法)的 demo:
查看视频,点击 Demo 的在线地址 自己来试试吧。
Web Worker
Chrome 浏览器中每个 Tab 都是一个进程,每个进程都会有一个主线程,网页的渲染(Style, Layout, Paint, Composite)会在主线程进行操作。主线程可以发起多个 Web Worker,Web Worker 对应“线程”的概念。
每个 Web Worker 都对应一个脚本文件,主线程可以通过像以下的代码去发起多个 Web Worker,并且通过基于事件的 API 与 Web Worker 通信:
main.js
let worker = new Worker("work.js");
worker.postMessage("Hello World");
worker.onmessage = function (event) {
console.log("Received message " + event.data);
}Web Worker 也通过相应的实现 API 与主线程进行通信
worker.js
this.addEventListener("message", function (e) {
this.postMessage("You said: " + e.data);
}, false);Web Worker 通讯的效率与同步问题
主线程与 Web Worker 通过 postMessage(data: any) 通信的时候,data会先被 copy 一份再传给 Web Worker;同样地,当 Web Worker 通过 postMessage(data: any) 与主线程通信的时候,data 也会同样先被 copy 一份再传给主线程。
这样做显然会导致通信上的效率问题,试想一下你需要在 Web Worker 里面解压一个 1G 大小的问题,你需要把整个 1G 的文件 copy 到 Web Worker 里,Web Worker 解压完这个 1G 文件后,再把解压完的文件 copy 回主线程里。
SharedArrayBuffer
为了解决通讯效率问题,浏览器提出了 ShareArrayBuffer,ShareArrayBuffer 基于 ArrayBuffer 和 TypedArray API。ArrayBuffer 对应一段内存(二进制内容),为了操作这段内存,浏览器需要提供一些视图(Int8Array 等),例如可以把这段内存当做每 8 位一个单元的 byte 数组,每 16 位一个单元的 16 位有符号数数组。
注意:ArrayBuffer 中的二进制流被翻译成各种视图的时候采用小端还是大端是由具体硬件决定的,绝大部分情况下是采用小端字节顺序。
这段内存可以在不同的 Worker 之间共享,但是内存的共享又会产生另外的问题,也就是竞争的问题(race onditions):
计算机指令对内存操作进行运算的时候,我们可以看做分两步:一是从内存中取值,二是运算并给某段内存赋值。当我们有两个线程对同一个内存地址进行 +1 操作的时候,假设线程是先后顺序运行的,为了简化模型,我们可以如下图表示:
上面两个线程的运行结果也符合我们的预期,也即线程分别都对同一地址进行了 +1 操作,最后得到结果 3。但因为两个线程是同时运行的,往往会发生下图所表示的问题,也即读取与写入可能不在一个事务中发生:
这种情况就叫做竞争问题(Race Condition)。
Atomics
为了解决上述的竞争问题,浏览器提供了 Atomics API,这组 API 是一组原子操作,可以将读取和写入绑定起来,例如下图中的 S1 到 S3 操作就被浏览器封装成 Atomics.add() 这个 API,从而解决竞争问题。
Atomics API 具体包含:
- Atomics.add()
- Atomics.and()
- Atomics.compareExchange()
- Atomics.exchange()
- Atomics.isLockFree()
- Atomics.load()
- Atomics.notify()
- Atomics.or()
- Atomics.store()
- Atomics.sub()
- Atomics.wait()
- Atomics.xor()
有了这套 API,我们可以实现像 Golang 中的 Golang Synchronization Primitives 的功能。Mutex 和 Cond 的实现会在下面介绍。
WebAssembly
有了 SharedArrayBuffer 和 Atomics 能力之后,证明浏览器能够提供内存共享和锁的实现了,也就是说 WebAssembly 线程在浏览器机制上能够高效地得到保证。
其实我严重怀疑 SharedArrayBuffer 和 Atomics 是为了支持 WebAssembly 才把 API 顺便提供给 JS Runtime 的,因为目前为止没有看到 ES 有比较丰富的关于锁的草案(例如像 Java 中的 synchronized 关键字)。
Mutext 和 Cond 的实现
上面提到了,基于 ShareArrayBuffer 和 Atomics 可以开发像 Golang Synchronization Primitives 一样的 API,下面介绍一下 Mutex 和 Cond 的实现。实现的介绍是基于 Mozzila Javascript 编译器工程师 Lars T Hansen 实现关于锁的库。
Mutex
首先说一下 Mutex 的功能,Mutex 的 API 大概是这样的:
let mutex = new Lock(shareArrayBuffer, ...);
mutex.lock();
doSomething();
mutex.unlock();Mutex 可以保证 lock() 和 unlock() 之间的代码代码不会被打断。下面是介绍具体实现:
首先定义 Mutex 的三个状态以及对应的状态机
- UNLOCK: 未锁定
- LOCKED: 被锁定
- WAITED: 被锁定且大于等于 1 个线程在等待该锁
对于 Worker 线程来说 Mutex 的每个状态都可能是初始态,状态与状态间扭转会产生一些操作且进入下一状态:
加锁 lock()
- 初始状态为UNLOCK: 锁未被抢占,将状态扭转为 LOCKED,线程进行后续操作。
- 初始状态为LOCKED: 锁已被抢占,将状态扭转为 WAITED,并将线程设置为等待态,并将线程设置为当锁的状态不为 WAITED 的时候可能被唤醒,一旦被唤醒则该线程拥有锁,线程进行后续操作。
- 初始状态为WAITED: 锁已被抢占,并将线程设置为等待态,并将线程设置为当锁的状态不为 WAITED 的时候可能被唤醒,一旦被唤醒则该线程拥有锁,线程进行后续操作。
释放 unlock()
1.初始状态为LOCKED: 锁被抢占且未被等待,将状态扭转为 UNLOCK,线程进行后续操作。
- 初始状态为WAITED: 锁被抢占且被等待,将状态扭转为 LOCKED,唤醒一个在等待态的线程,线程进行后续操作。
上面描述的逻辑的对应的代码如下:
// lock
Lock.prototype.lock = function () {
const iab = this._iab;
const stateIdx = this._ibase;
let c;
if ((c = Atomics.compareExchange(iab, stateIdx, 0, 1)) != 0) {
do {
if (c == 2 || Atomics.compareExchange(iab, stateIdx, 1, 2) != 0)
Atomics.wait(iab, stateIdx, 2);
} while ((c = Atomics.compareExchange(iab, stateIdx, 0, 2)) != 0);
}
}
// unlock
Lock.prototype.unlock = function () {
const iab = this._iab;
const stateIdx = this._ibase;
let v0 = Atomics.sub(iab, stateIdx, 1);
// Wake up a waiter if there are any
if (v0 != 1) {
Atomics.store(iab, stateIdx, 0);
Atomics.notify(iab, stateIdx, 1);
}
}可以看到锁的实现用到了 Atomics.compareExchange() 和 Atomics.wait()(相当于 Linux 中的 futex)两个原子操作。
Cond
Cond 是基于 Mutex 实现的,它的大致功能是持有锁的情况下可进行两种操作:
- wait(): 本线程进度进入等待态,并且被唤醒的时候重新持有锁。
- notifyOne(): 唤醒一个正在等待态的线程。
具体使用方法如下:
// thread A
var msg = new Int32Array(sab, msgLoc, 1);
lock.lock();
while (msg[0] < numWorkers)
cond.wait();
lock.unlock();
// thread B, C, D, E, …
var msg = new Int32Array(sab, msgLoc, 1);
lock.lock();
msg[0]++;
cond.notifyOne();
lock.unlock();由于 Cond 是基于 Mutex,前置条件是持有锁,后置条件是释放锁,你可以看做 Cond 只有两个状态:
- NORMAL: 非等待态,调用 wait() 转化为 WAITED 状态,并把线程设置为等待态,并且被唤醒的时候重新持有锁,然后进行后续操作。
- WAITED: 等待态(不对应上述 Lock 的 WAITED 态),调用 notifyOne() 将状态设置为 NORMAL 态,重新唤醒一个处于等待态的线程,然后进行后续操作。
异步锁
上述介绍的锁都是同步的,Atomics.wait 不能在主线程使用,在主线程使用的话浏览器会抛出异常:
Uncaught TypeError: Atomics.wait cannot be called in this context
所以我们需要设计所谓的”异步锁“,所谓的异步锁原理很简单,就是将同步锁里面的 Atomics.wait() 操作交给一个新的线程,主线程和这个线程通过事件通信来异步化这里的操作。具体实现可以参照这个文件)。
demo 实现
介绍完上述的知识之后,就可以用相关的 API 就可以实现我们的 demo 了,首先画一下我们 demo 的架构图:
如图所示,在线解压缩这个 demo 主要分为两个线程:
- 主线程:负责调用 Dom API 等,主要负责 UI 更新。
- 工作线程:负责文件的压缩/解压。
两个线程间的通信是通过读写两段共享内存来实现的,对于共享内存的访问,通过锁来解决竞争问题。需要注意的是,主线程的写缓存也即工作线程的读缓存,反之亦然。
demo 的具体实现可以参照 demo 的 Github 地址。
目前多线程编程的不足
目前只通过浏览器提供的 API 来进多线程开发的话成本非常大,主要有两方面问题:
过于底层的 API
- 需要你实现语言级、或者系统级的 lock API,参照 Golang 的 lock API。
- 没有语法上的支持,例如 Java synchronized 关键字等。
普通的 Javascript Object 无法共享
这其实也是 API 过于底层的另一方面的体现,也就是说对 JS 对象进行内存共享的话,你需要开辟一段 SharedArrayBuffer,然后在此之上实现对 JS 对象的序列化、反序列化、更新等操作,实现成本也是比较大的。
事实上我们也不应该轻易手动实现相关的库或者功能,因为相关领域的问题非常复杂、需要仔细的设计和实现。例如我们可以先使用下面这两个库:
- parlib-simple: 这个库里面有类似于 Golang 里面 channel 一样的 API。
- js-lock-and-condition: 这里库有 Mutex 和 Cond 实现。
总结
浏览器提供给了我们进行多线程的能力,例如 PWA 或者 WebAseembly 与 JS 混用等场景都会用到上述的机制,如果你想实现一个高性能的网页客户端程序(例如 Figma 一样的杀手级应用),你最好也用上上述的机制。值得注意的是,用了锁可能会降低你的程序的性能,具体要看线程切换和等待是的成本是否能够抵消内存拷贝的成本,例如 demo 完全可以改成无锁的,代价将文件内容拷贝到共享线程,并把工作线程的内容拷贝回主线程。
虽然上面建议不要轻易实现自己的库,例如上面的 lock 代码短短几行,但是其中的推导可以足够写十几页的 Paper 了,但是这里的基础能力很匮乏,据笔者了解,TC39 提案中鲜少出现关于多线程编程的提案,目前仅发现以下这个:
- proposal-atomics-wait-async
但是,如果自信有能力和时间建设这些基础能力的话,这个领域的确是“广阔天地,大有作为”,特别是如果你的项目准备用 WebAseembly 和 JS 混用的情况(例如 Figma 就是用了 WebAssembly 和 React)。
推荐JavaScript经典实例学习资料文章
《图解 Promise 实现原理(二):Promise 链式调用》
《图解 Promise 实现原理(三):Promise 原型方法实现》
《图解 Promise 实现原理(四):Promise 静态方法实现》
《使用Service Worker让你的 Web 应用如虎添翼(上)「干货」》
《使用Service Worker让你的 Web 应用如虎添翼(中)「干货」》
《使用Service Worker让你的 Web 应用如虎添翼(下)「干货」》
《一个轻量级 JavaScript 全文搜索库,轻松实现站内离线搜索》
《细品269个JavaScript小函数,让你少加班熬夜(一)「值得收藏」》
《细品269个JavaScript小函数,让你少加班熬夜(二)「值得收藏」》
《细品269个JavaScript小函数,让你少加班熬夜(三)「值得收藏」》
《细品269个JavaScript小函数,让你少加班熬夜(四)「值得收藏」》
《细品269个JavaScript小函数,让你少加班熬夜(五)「值得收藏」》
《细品269个JavaScript小函数,让你少加班熬夜(六)「值得收藏」》
《手把手教你7个有趣的JavaScript 项目-上「附源码」》
《手把手教你7个有趣的JavaScript 项目-下「附源码」》
《JavaScript 使用 mediaDevices API 访问摄像头自拍》
《一文彻底搞懂JavaScript 中Object.freeze与Object.seal的用法》
《可视化的 JS:动态图演示 - 事件循环 Event Loop的过程》
《可视化的 js:动态图演示 Promises & Async/Await 的过程》
《Pug 3.0.0正式发布,不再支持 Node.js 6/8》
《通过发布/订阅的设计模式搞懂 Node.js 核心模块 Events》
《「速围」Node.js V14.3.0 发布支持顶级 Await 和 REPL 增强功能》
《JavaScript 已进入第三个时代,未来将何去何从?》
《前端上传前预览文件 image、text、json、video、audio「实践」》
《深入细品 EventLoop 和浏览器渲染、帧动画、空闲回调的关系》
《推荐13个有用的JavaScript数组技巧「值得收藏」》
《36个工作中常用的JavaScript函数片段「值得收藏」》
《一文了解文件上传全过程(1.8w字深度解析)「前端进阶必备」》
《手把手教你如何编写一个前端图片压缩、方向纠正、预览、上传插件》
《JavaScript正则深入以及10个非常有意思的正则实战》
《前端开发规范:命名规范、html规范、css规范、js规范》
《100个原生JavaScript代码片段知识点详细汇总【实践】》
《手把手教你深入巩固JavaScript知识体系【思维导图】》
《一个合格的中级前端工程师需要掌握的 28 个 JavaScript 技巧》
《身份证号码的正则表达式及验证详解(JavaScript,Regex)》
《127个常用的JS代码片段,每段代码花30秒就能看懂-【上】》
《深入浅出讲解JS中this/apply/call/bind巧妙用法【实践】》
《干货满满!如何优雅简洁地实现时钟翻牌器(支持JS/Vue/React)》
《面试中教你绕过关于 JavaScript 作用域的 5 个坑》
作者: jolamjiang 腾讯技术工程
转发链接:
https://mp.weixin.qq.com/s/87C9GAFb0Y_i5iPbIL5Hzg
相关推荐
- 如何设计一个优秀的电子商务产品详情页
-
加入人人都是产品经理【起点学院】产品经理实战训练营,BAT产品总监手把手带你学产品电子商务网站的产品详情页面无疑是设计师和开发人员关注的最重要的网页之一。产品详情页面是客户作出“加入购物车”决定的页面...
- 怎么在JS中使用Ajax进行异步请求?
-
大家好,今天我来分享一项JavaScript的实战技巧,即如何在JS中使用Ajax进行异步请求,让你的网页速度瞬间提升。Ajax是一种在不刷新整个网页的情况下与服务器进行数据交互的技术,可以实现异步加...
- 中小企业如何组建,管理团队_中小企业应当如何开展组织结构设计变革
-
前言写了太多关于产品的东西觉得应该换换口味.从码农到架构师,从前端到平面再到UI、UE,最后走向了产品这条不归路,其实以前一直再给你们讲.产品经理跟项目经理区别没有特别大,两个岗位之间有很...
- 前端监控 SDK 开发分享_前端监控系统 开源
-
一、前言随着前端的发展和被重视,慢慢的行业内对于前端监控系统的重视程度也在增加。这里不对为什么需要监控再做解释。那我们先直接说说需求。对于中小型公司来说,可以直接使用三方的监控,比如自己搭建一套免费的...
- Ajax 会被 fetch 取代吗?Axios 怎么办?
-
大家好,很高兴又见面了,我是"高级前端进阶",由我带着大家一起关注前端前沿、深入前端底层技术,大家一起进步,也欢迎大家关注、点赞、收藏、转发!今天给大家带来的主题是ajax、fetch...
- 前端面试题《AJAX》_前端面试ajax考点汇总
-
1.什么是ajax?ajax作用是什么?AJAX=异步JavaScript和XML。AJAX是一种用于创建快速动态网页的技术。通过在后台与服务器进行少量数据交换,AJAX可以使网页实...
- Ajax 详细介绍_ajax
-
1、ajax是什么?asynchronousjavascriptandxml:异步的javascript和xml。ajax是用来改善用户体验的一种技术,其本质是利用浏览器内置的一个特殊的...
- 6款可替代dreamweaver的工具_替代powerdesigner的工具
-
dreamweaver对一个web前端工作者来说,再熟悉不过了,像我07年接触web前端开发就是用的dreamweaver,一直用到现在,身边的朋友有跟我推荐过各种更好用的可替代dreamweaver...
- 我敢保证,全网没有再比这更详细的Java知识点总结了,送你啊
-
接下来你看到的将是全网最详细的Java知识点总结,全文分为三大部分:Java基础、Java框架、Java+云数据小编将为大家仔细讲解每大部分里面的详细知识点,别眨眼,从小白到大佬、零基础到精通,你绝...
- 福斯《死侍》发布新剧照 "小贱贱"韦德被改造前造型曝光
-
时光网讯福斯出品的科幻片《死侍》今天发布新剧照,其中一张是较为罕见的死侍在被改造之前的剧照,其余两张剧照都是死侍在执行任务中的状态。据外媒推测,片方此时发布剧照,预计是为了给不久之后影片发布首款正式预...
- 2021年超详细的java学习路线总结—纯干货分享
-
本文整理了java开发的学习路线和相关的学习资源,非常适合零基础入门java的同学,希望大家在学习的时候,能够节省时间。纯干货,良心推荐!第一阶段:Java基础重点知识点:数据类型、核心语法、面向对象...
- 不用海淘,真黑五来到你身边:亚马逊15件热卖爆款推荐!
-
Fujifilm富士instaxMini8小黄人拍立得相机(黄色/蓝色)扫二维码进入购物页面黑五是入手一个轻巧可爱的拍立得相机的好时机,此款是mini8的小黄人特别版,除了颜色涂装成小黄人...
- 2025 年 Python 爬虫四大前沿技术:从异步到 AI
-
作为互联网大厂的后端Python爬虫开发,你是否也曾遇到过这些痛点:面对海量目标URL,单线程爬虫爬取一周还没完成任务;动态渲染的SPA页面,requests库返回的全是空白代码;好不容易...
- 最贱超级英雄《死侍》来了!_死侍超燃
-
死侍Deadpool(2016)导演:蒂姆·米勒编剧:略特·里斯/保罗·沃尼克主演:瑞恩·雷诺兹/莫蕾娜·巴卡林/吉娜·卡拉诺/艾德·斯克林/T·J·米勒类型:动作/...
- 停止javascript的ajax请求,取消axios请求,取消reactfetch请求
-
一、Ajax原生里可以通过XMLHttpRequest对象上的abort方法来中断ajax。注意abort方法不能阻止向服务器发送请求,只能停止当前ajax请求。停止javascript的ajax请求...
- 一周热门
- 最近发表
- 标签列表
-
- HTML 简介 (30)
- HTML 响应式设计 (31)
- HTML URL 编码 (32)
- HTML Web 服务器 (31)
- HTML 表单属性 (32)
- HTML 音频 (31)
- HTML5 支持 (33)
- HTML API (36)
- HTML 总结 (32)
- HTML 全局属性 (32)
- HTML 事件 (31)
- HTML 画布 (32)
- HTTP 方法 (30)
- 键盘快捷键 (30)
- CSS 语法 (35)
- CSS 轮廓宽度 (31)
- CSS 谷歌字体 (33)
- CSS 链接 (31)
- CSS 定位 (31)
- CSS 图片库 (32)
- CSS 图像精灵 (31)
- SVG 文本 (32)
- 时钟启动 (33)
- HTML 游戏 (34)
- JS Loop For (32)
