JavaScript 中的多线程编程

字节逆旅

共 4558字,需浏览 10分钟

 ·

2022-08-02 19:08

多线程意味着使用操作系统可以分配多核资源给程序实现真正的并行计算,能否实现多线程编程取决于运行时环境和编程语言对多线程编程的 API 支持与否。

当一提到 JavaScript,大家很多人都想到“单线程”、“非阻塞”、“事件驱动”、“高并发”等术语。事实上这些印象并不准确,借助于 Web Worker、资源共享和原子操作,浏览器端可以实现高性能的多线程,而 Node.js 借助多线程库也可以实现多线程。

本文包含以下内容:

  • Process & Thread
  • JavaScript Runtime Differences
  • TypedArray
  • ArrayBuffer
  • Atomics
  • JS Multithreading In Chrome

Process & Thread

进程是资源分配的最小单位,拥有地址空间、全局变量、堆等资源;线程是系统调度的最小单位,拥有独立的程序计数器、执行栈、寄存器、状态字等资源。

线程自己基本上不拥有系统资源,只拥有一点在运行中必不可少的资源,它与同属一个进程的其他的线程共享进程所拥有的全部资源。线程切换的时候实际上切换的是一个可以称之为线程控制块的结构(TCB?),里面保存所有将来用于恢复线程环境必需的信息,包括所有必需保存的寄存器集,线程的状态等。

JavaScript Runtime Differences

我们可以来对比的看一下浏览器(以 Chrome 为例)和 Node.js 环境的差异:

关于 Chrome 的程序模型在大白话聊前端优化中已经讲的很明白了,大致是这样的:

  • Chrome (Browser进程)
  • Tab1 (Renderer进程)
  • Tab2 (Renderer进程)
  • Iframe1 (Renderer进程)
  • TabN...
  • Extension1 Background Page(Extension进程)
  • Extension2 Background Page(Extension进程)
  • ExtensionN Background Page...
  • GPU Renderer进程
  • ...

每种进程有不一样的作用:

  • Browser进程:负责与用户交互,管理 Render 进程,协调网络等;
  • Renderer进程:负责每个页面的脚本、绘制、事件循环等;(资源包括,数据堆,函数执行栈,任务队列)
    • 事件循环就是不停的检查是否有可以处理的事件,有的话就交给 JS解释器线程
    • 单线程:为了减少复杂度——多个解释器修改DOM
    • 非阻塞:线程永远不会被挂起——不会位于阻塞态
    • JS解释器线程:只有一个,负责处理 Task Queue 中的任务,解释执行脚本
    • GUI渲染线程:只有一个,负责绘制 DOM Tree、CSSOM Tree 和 RenderObject Tree,与JS解释器线程互斥且权重较低
    • 事件循环线程:负责维护 Task Queue 的运行
    • 定时器线程:处理定时器事件,将事件回调提交给 Task Queue,精度4ms
    • 网络线程:可以有多个,每个处理一个网络请求,将事件回调提交给 Task Queue
    • Dedicated Worker线程:可以有多个,负责执行 JS解释器线程分配的后台计算任务
  • Exntension进程:和Renderer进程差不多,不过浏览器扩展的背景页没有GUI渲染的需求,所以主要是执行JS;
  • GPU Renderer进程:使用GPU渲染时用到,需要开启硬件加速
  • Service Worker线程:可以有多个,业务上负责通过注册的域的网络事件来触发计算,位于独立的进程中,无状态以域为scope;
  • Shared Worker线程:Web Worker的一种,位于独立的后台进程中,类似于 Dedicated Worker,使用 MessagePort 与多个页面通信;

事实上,在 Chrome 中的异步操作比如定时器(setTimeout、setInterval)事件、Ajax请求、I/O回调等都是通过多线程来实现的。Chrome 支持我们通过三种 Web Worker 线程来实现多线程编程:

  • Dedicated Worker,和 Tab 中的 JavaScript 解释器线程同进程;
  • Shared Worker,位于独立的后台进程中,类似于 Dedicated Worker,使用 MessagePort 与多个页面通信;
  • Service Worker线程:可以有多个,业务上负责通过注册的域的网络事件来触发计算,位于独立的进程中,无状态以域为scope;

和浏览器不同,Node.js 的异步操作比如定时器(setTimeout、setInterval)事件、Ajax请求、I/O回调等都是通过 event polling 来实现的,每一次的 polling 由许许多多的 tick 组成。

TypedArray

类型化数组(TypedArray)对象描述了一个底层的二进制数据缓冲区(binary data buffer)的一个类数组视图(view)。事实上,没有名为 TypedArray 的全局属性,也没有一个名为 TypedArray 的构造函数。所有的类型化数组构造器都会继承 %TypeArray% 构造器函数的公共属性和方法。

当创建一个 TypedArray 实例(如 Int8Array)时,一个数组缓冲区将被创建在内存中,如果一个 ArrayBuffer 对象被当作参数传给构造函数,那么将使用传入的 ArrayBuffer 代替(即缓冲区被创建到 ArrayBuffer 中)。缓冲区的地址被存储在实例的内部属性中,并且所有 %TypedArray%.prototype上的方法,例如 set value 和 get value 等,都会在这个数组缓冲区上进行操作。

ArrayBuffer

ArrayBuffer 对象用来表示通用的、固定长度的原始二进制数据缓冲区。它是一个字节数组,通常在其他语言中称为“byte array”。ArrayBuffer 构造函数用来创建一个指定字节长度的 ArrayBuffer 对象。

ArrayBuffer 仅仅是一个个 0/1 组成的串,它不知道第一个元素和第二个元素的分割点。所以我们需要 TypedArray 作为 view 来划分。ArrayBuffer.isView() 方法用来判断传入的参数值是否是一种 ArrayBuffer 视图(view),比如类型化数组对象(typed array objects)或者数据视图( DataView)。

ArrayBuffer可以被 self.postMessage(arr.buffer, [arr.buffer]); 发送到另一个线程。把某个特定区域的内存移过去后其它线程就可以直接访问了。但是,之前的线程就无法访问了。

SharedArrayBuffer 可以使多个线程共用一块儿内存资源(跨进程也可以),几乎没有延时,但是还是需要原子操作和竞争。为了将一个SharedArrayBuffer 对象从一个用户代理共享到另一个用户代理(另一个页面的主进程或者当前页面的一个 worker )从而实现共享内存,我们需要运用 postMessage 和结构化克隆算法( structured cloning )。两个 SharedArrayBuffer 对象指向的内存其实是同一个,并且在某一代理中的操作将最终对另一个代理可见。

Atomics

Atomics 对象提供了一组静态方法对 SharedArrayBuffer 和  ArrayBuffer 对象进行原子操作。Atomics 的所有属性和方法都是静态的(与 Math  对象一样)。

多个共享内存的线程能够同时读写同一位置上的数据。原子操作会确保正在读或写的数据的值是符合预期的,即下一个原子操作一定会在上一个原子操作结束后才会开始,其操作过程不会中断。

// 对 SharedArrayBuffer Item 进行数模运算Atomics.add()Atomics.sub()
Atomics.and()Atomics.or()Atomics.xor()
// 对 SharedArrayBuffer Item 进行获取、替换、写入Atomics.load()Atomics.exchange()Atomics.compareExchange()Atomics.store()
// 通过 SharedArrayBuffer 进行跨线程原子操作Atomics.notify()Atomics.wait()

JS Multithreading In Chrome

Chrome 支持我们通过三种 Web Worker 线程来实现多线程编程:

  • Dedicated Worker,和 Tab 中的 JavaScript 解释器线程同进程;
  • Shared Worker,位于独立的后台进程中,类似于 Dedicated Worker,使用 MessagePort 与多个页面通信;
  • Service Worker线程:可以有多个,业务上负责通过注册的域的网络事件来触发计算,位于独立的进程中,无状态以域为scope;

Web Worker 线程程序的载入是通过单独的文件来实现的,比如放在特定域的根目录下。拿 Dedicated Worker 举例:

/* main.js */if ('Worker' in window) {  const worker = new Worker('worker.js')  $first.onchange = () => {    worker.postMessage([first.value, second.value])    console.log('Message posted to worker')  }  worker.onmessage = (e) => {    result.textContent = e.data    console.log('Message received from worker')  }}
/* worker.js */function onmessage(e) { console.log('Worker: Message received from main script') const result = e.data[0] * e.data[1] postMessage(workerResult)}

通过 postMessage API 可以实现浏览器不同线程之间的通讯,通过监听器回调的方式是多线程编程比较简单的实现。

如果要追求高性能低延时,则需要使用 SharedArrayBuffer 和 Atomics。通过 SharedArrayBuffer 分配一块儿共享的内存资源,然后使用 Atomics 来限制资源竞争。这样是非常高效的,但是底层意味着复杂性的快速增加。


希望对你有帮助,如果有用就请点赞分享在看,谢谢鼓励!

扫码关注 字节逆旅 公众号,为您提供更多技术干货!


浏览 96
点赞
评论
收藏
分享

手机扫一扫分享

分享
举报
评论
图片
表情
推荐
点赞
评论
收藏
分享

手机扫一扫分享

分享
举报