第 27 章 工作者线程
27.1 工作者线程简介
27.1.1 工作者线程与线程
作为介绍,通常需要将工作者线程与执行线程进行比较。在许多方面,这是一个恰当的比较,因为工作者线程和线程确实有很多共同之处。
❑ 工作者线程是以实际线程实现的。例如,Blink 浏览器引擎实现工作者线程的 WorkerThread 就对应着底层的线程。
❑ 工作者线程并行执行。虽然页面和工作者线程都是单线程 JavaScript 环境,每个环境中的指令则可以并行执行。
❑ 工作者线程可以共享某些内存。工作者线程能够使用 SharedArrayBuffer 在多个环境间共享内容。虽然线程会使用锁实现并发控制,但 JavaScript 使用 Atomics 接口实现并发控制。
工作者线程与线程有很多类似之处,但也有重要的区别。
❑ 工作者线程不共享全部内存。在传统线程模型中,多线程有能力读写共享内存空间。除了 Shared-ArrayBuffer 外,从工作者线程进出的数据需要复制或转移。
❑ 工作者线程不一定在同一个进程里。通常,一个进程可以在内部产生多个线程。根据浏览器引擎的实现,工作者线程可能与页面属于同一进程,也可能不属于。例如,Chrome 的 Blink 引擎对共享工作者线程和服务工作者线程使用独立的进程。
❑ 创建工作者线程的开销更大。工作者线程有自己独立的事件循环、全局对象、事件处理程序和其他 JavaScript 环境必需的特性。创建这些结构的代价不容忽视
27.1.2 工作者线程的类型
Web 工作者线程规范中定义了三种主要的工作者线程:专用工作者线程、共享工作者线程和服务工作者线程。
1.专用工作者线程
专用工作者线程,通常简称为工作者线程、Web Worker 或 Worker,是一种实用的工具,可以让脚本单独创建一个 JavaScript 线程,以执行委托的任务。专用工作者线程,顾名思义,只能被创建它的页面使用。
2.共享工作者线程
共享工作者线程与专用工作者线程非常相似。主要区别是共享工作者线程可以被多个不同的上下文使用,包括不同的页面。任何与创建共享工作者线程的脚本同源的脚本,都可以向共享工作者线程发送消息或从中接收消息。
3.服务工作者线程
服务工作者线程与专用工作者线程和共享工作者线程截然不同。它的主要用途是拦截、重定向和修改页面发出的请求,充当网络请求的仲裁者的角色。
27.1.3 WorkerGlobalScope
在网页上,window 对象可以向运行在其中的脚本暴露各种全局变量。在工作者线程内部,没有 window 的概念。这里的全局对象是 WorkerGlobalScope 的实例,通过 self 关键字暴露出来。
1.WorkerGlobalScope 属性和方法
self 上可用的属性是 window 对象上属性的严格子集。其中有些属性会返回特定于工作者线程的版本。
❑ navigator:返回与工作者线程关联的 WorkerNavigator。
❑ self:返回 WorkerGlobalScope 对象。
❑ location:返回与工作者线程关联的 WorkerLocation。
❑ performance:返回(只包含特定属性和方法的)Performance 对象。
❑ console:返回与工作者线程关联的 Console 对象;对 API 没有限制。
❑ caches:返回与工作者线程关联的 CacheStorage 对象;对 API 没有限制。
❑ indexedDB:返回 IDBFactory 对象。
❑ isSecureContext:返回布尔值,表示工作者线程上下文是否安全。
❑ origin:返回 WorkerGlobalScope 的源。
类似地,self 对象上暴露的一些方法也是 window 上方法的子集。这些 self 上的方法也与 window 上对应的方法操作一样。
❑ atob()
❑ btoa()
❑ clearInterval()
❑ clearTimeout()
❑ createImageBitmap()
❑ fetch()
❑ setInterval()
❑ setTimeout()
WorkerGlobalScope 还增加了新的全局方法 importScripts(),只在工作者线程内可用。
2.WorkerGlobalScope 的子类
实际上并不是所有地方都实现了 WorkerGlobalScope。每种类型的工作者线程都使用了自己特定的全局对象,这继承自 WorkerGlobalScope。
❑ 专用工作者线程使用 DedicatedWorkerGlobalScope。
❑ 共享工作者线程使用 SharedWorkerGlobalScope。
❑ 服务工作者线程使用 ServiceWorkerGlobalScope。
27.2 专用工作者线程
专用工作者线程是最简单的 Web 工作者线程,网页中的脚本可以创建专用工作者线程来执行在页面线程之外的其他任务。这样的线程可以与父页面交换信息、发送网络请求、执行文件输入/输出、进行密集计算、处理大量数据,以及实现其他不适合在页面执行线程里做的任务(否则会导致页面响应迟钝)。
27.2.1 专用工作者线程的基本概念
可以把专用工作者线程称为后台脚本(background script)。JavaScript 线程的各个方面,包括生命周期管理、代码路径和输入/输出,都由初始化线程时提供的脚本来控制。该脚本也可以再请求其他脚本,但一个线程总是从一个脚本源开始。
1.创建专用工作者线程
创建专用工作者线程最常见的方式是加载 JavaScript 文件。把文件路径提供给 Worker 构造函数,然后构造函数再在后台异步加载脚本并实例化工作者线程。传给构造函数的文件路径可以是多种形式。
// emptyWorker.js
// 空的JS工作者线程文件
// main.js
console.log(location.href) // "https://example.com/"
const worker = new Worker(location.href + 'emptyWorker.js') // 绝对路径
const worker = new Worker('./emptyWorker.js') /// 相对路径
console.log(worker) // Worker {}❑ 这个文件是在后台加载的,工作者线程的初始化完全独立于 main.js。
❑ 工作者线程本身存在于一个独立的 JavaScript 环境中,因此 main.js 必须以 Worker 对象为代理实现与工作者线程通信。在上面的例子中,该对象被赋值给了 worker 变量。
❑ 虽然相应的工作者线程可能还不存在,但该 Worker 对象已在原始环境中可用了。
2.工作者线程安全限制
工作者线程的脚本文件只能从与父页面相同的源加载。从其他源加载工作者线程的脚本文件会导致错误。
// 尝试基于https://example.com/worker.js创建工作者线程
const sameOriginWorker = new Worker('./worker.js')
// 尝试基于https://untrusted.com/worker.js创建工作者线程
const remoteOriginWorker = new Worker('https://untrusted.com/worker.js')
// Error: UncaughtDOMException: Failedtoconstruct'Worker':
// Script at https://untrusted.com/main.js can not be accessed
// from origin https://example.com基于加载脚本创建的工作者线程不受文档的内容安全策略限制,因为工作者线程在与父文档不同的上下文中运行。不过,如果工作者线程加载的脚本带有全局唯一标识符(与加载自一个二进制大文件一样),就会受父文档内容安全策略的限制。
3.使用 Worker 对象
Worker()构造函数返回的 Worker 对象是与刚创建的专用工作者线程通信的连接点。它可用于在工作者线程和父上下文间传输信息,以及捕获专用工作者线程发出的事件。
注意
要管理好使用 Worker()创建的每个 Worker 对象。在终止工作者线程之前,它不会被垃圾回收,也不能通过编程方式恢复对之前 Worker 对象的引用。
Worker 对象支持下列事件处理程序属性。
❑ onerror:在工作者线程中发生 ErrorEvent 类型的错误事件时会调用指定给该属性的处理程序。
■ 该事件会在工作者线程中抛出错误时发生。
■ 该事件也可以通过 worker.addEventListener('error', handler)的形式处理。
❑ onmessage:在工作者线程中发生 MessageEvent 类型的消息事件时会调用指定给该属性的处理程序。
■ 该事件会在工作者线程向父上下文发送消息时发生。
■ 该事件也可以通过使用 worker.addEventListener('message', handler)处理。
❑ onmessageerror:在工作者线程中发生 MessageEvent 类型的错误事件时会调用指定给该属性的处理程序。
■ 该事件会在工作者线程收到无法反序列化的消息时发生。
■ 该事件也可以通过使用 worker.addEventListener('messageerror',handler)处理。
Worker 对象还支持下列方法。
❑ postMessage():用于通过异步消息事件向工作者线程发送信息。
❑ terminate():用于立即终止工作者线程。没有为工作者线程提供清理的机会,脚本会突然停止。
4.DedicatedWorkerGlobalScope
在专用工作者线程内部,全局作用域是 DedicatedWorkerGlobalScope 的实例。因为这继承自 WorkerGlobalScope,所以包含它的所有属性和方法。工作者线程可以通过 self 关键字访问该全局作用域。
// globalScopeWorker.js
console.log('insideworker:', self)
// main.js
const worker = new Worker('./globalScopeWorker.js')
console.log('createdworker:', worker)
// created worker: Worker {}
// inside worker: DedicatedWorkerGlobalScope {}如此例所示,顶级脚本和工作者线程中的 console 对象都将写入浏览器控制台,这对于调试非常有用。因为工作者线程具有不可忽略的启动延迟,所以即使 Worker 对象存在,工作者线程的日志也会在主线程的日志之后打印出来。
注意
这里两个独立的 JavaScript 线程都在向一个 console 对象发消息,该对象随后将消息序列化并在浏览器控制台打印出来。浏览器从两个不同的 JavaScript 线程收到消息,并按照自己认为合适的顺序输出这些消息。为此,在多线程应用程序中使用日志确定操作顺序时必须要当心。
DedicatedWorkerGlobalScope 在 WorkerGlobalScope 基础上增加了以下属性和方法。
❑ name:可以提供给 Worker 构造函数的一个可选的字符串标识符。
❑ postMessage():与 worker.postMessage()对应的方法,用于从工作者线程内部向父上下文发送消息。
❑ close():与 worker.terminate()对应的方法,用于立即终止工作者线程。没有为工作者线程提供清理的机会,脚本会突然停止。
❑ importScripts():用于向工作者线程中导入任意数量的脚本。
27.2.2 专用工作者线程与隐式 MessagePorts
专用工作者线程的 Worker 对象和 DedicatedWorkerGlobalScope 与 MessagePorts 有一些相同接口处理程序和方法:onmessage、onmessageerror、close()和 postMessage()。这不是偶然的,因为专用工作者线程隐式使用了 MessagePorts 在两个上下文之间通信。
父上下文中的 Worker 对象和 DedicatedWorkerGlobalScope 实际上融合了 MessagePort,并在自己的接口中分别暴露了相应的处理程序和方法。换句话说,消息还是通过 MessagePort 发送,只是没有直接使用 MessagePort 而已。
也有不一致的地方,比如 start()和 close()约定。专用工作者线程会自动发送排队的消息,因此 start()也就没有必要了。另外,close()在专用工作者线程的上下文中没有意义,因为这样关闭 MessagePort 会使工作者线程孤立。因此,在工作者线程内部调用 close()(或在外部调用 terminate())不仅会关闭 MessagePort,也会终止线程。
27.2.3 专用工作者线程的生命周期
一般来说,专用工作者线程可以非正式区分为处于下列三个状态:初始化(initializing)、活动(active)和终止(terminated)。这几个状态对其他上下文是不可见的。虽然 Worker 对象可能会存在于父上下文中,但也无法通过它确定工作者线程当前是处理初始化、活动还是终止状态。换句话说,与活动的专用工作者线程关联的 Worker 对象和与终止的专用工作者线程关联的 Worker 对象无法分别。
初始化时,虽然工作者线程脚本尚未执行,但可以先把要发送给工作者线程的消息加入队列。这些消息会等待工作者线程的状态变为活动,再把消息添加到它的消息队列。
// initializingWorker.js
self.addEventListener('message', ({ data }) => console.log(data))
// main.js
const worker = new Worker('./initializingWorker.js')
// Worker可能仍处于初始化状态
// 但postMessage()数据可以正常处理
worker.postMessage('foo')
worker.postMessage('bar')
worker.postMessage('baz')
//foo
//bar
//baz创建之后,专用工作者线程就会伴随页面的整个生命期而存在,除非自我终止(self.close())或通过外部终止(worker.terminate())。即使线程脚本已运行完成,线程的环境仍会存在。只要工作者线程仍存在,与之关联的 Worker 对象就不会被当成垃圾收集掉。
自我终止和外部终止最终都会执行相同的工作者线程终止例程。来看下面的例子,其中工作者线程在发送两条消息中间执行了自我终止:
// closeWorker.js
self.postMessage('foo')
self.close()
self.postMessage('bar')
setTimeout(() => self.postMessage('baz'), 0)
// main.js
const worker = new Worker('./closeWorker.js')
worker.onmessage = ({ data }) => console.log(data)
//foo
//bar虽然调用了 close(),但显然工作者线程的执行并没有立即终止。close()在这里会通知工作者线程取消事件循环中的所有任务,并阻止继续添加新任务。这也是为什么"baz"没有打印出来的原因。工作者线程不需要执行同步停止,因此在父上下文的事件循环中处理的"bar"仍会打印出来。
下面来看外部终止的例子。
// terminateWorker.js
self.onmessage = ({ data }) => console.log(data)
// main.js
const worker = new Worker('./terminateWorker.js')
// 给1000 毫秒让工作者线程初始化
setTimeout(() => {
worker.postMessage('foo')
worker.terminate()
worker.postMessage('bar')
setTimeout(() => worker.postMessage('baz'), 0)
}, 1000)
//foo27.2.4 配置 Worker 选项
Worker()构造函数允许将可选的配置对象作为第二个参数。该配置对象支持下列属性。
❑ name:可以在工作者线程中通过 self.name 读取到的字符串标识符。
❑ type:表示加载脚本的运行方式,可以是"classic"或"module"。"classic"将脚本作为常规脚本来执行,"module"将脚本作为模块来执行。
❑ credentials:在 type 为"module"时,指定如何获取与传输凭证数据相关的工作者线程模块脚本。值可以是"omit"、"same-orign"或"include"。这些选项与 fetch()的凭证选项相同。在 type 为"classic"时,默认为"omit"。
27.2.5 在 JavaScript 行内创建工作者线程
工作者线程需要基于脚本文件来创建,但这并不意味着该脚本必须是远程资源。专用工作者线程也可以通过 Blob 对象 URL 在行内脚本创建。这样可以更快速地初始化工作者线程,因为没有网络延迟。
下面展示了一个在行内创建工作者线程的例子。
// 创建要执行的JavaScript代码字符串
const workerScript = `self.onmessage=({data})=>console.log(data);`
// 基于脚本字符串生成Blob对象
const workerScriptBlob = new Blob([workerScript])
// 基于Blob实例创建对象URL
const workerScriptBlobUrl = URL.createObjectURL(workerScriptBlob)
// 基于对象URL创建专用工作者线程
const worker = new Worker(workerScriptBlobUrl)
worker.postMessage('blob worker script')
// blob worker script在这个例子中,通过脚本字符串创建了 Blob,然后又通过 Blob 创建了对象 URL,最后把对象 URL 传给了 Worker()构造函数。该构造函数同样创建了专用工作者线程。
工作者线程也可以利用函数序列化来初始化行内脚本。这是因为函数的 toString()方法返回函数代码的字符串,而函数可以在父上下文中定义但在子上下文中执行。
function fibonacci(n) {
return n < 1 ? 0 : n <= 2 ? 1 : fibonacci(n - 1) + fibonacci(n - 2)
}
const workerScript = `
self.postMessage(
(${fibonacci.toString()})(9)
);
`
const worker = new Worker(URL.createObjectURL(new Blob([workerScript])))
worker.onmessage = ({ data }) => console.log(data)
//3427.2.6 在工作者线程中动态执行脚本
工作者线程中的脚本并非铁板一块,而是可以使用 importScripts()方法通过编程方式加载和执行任意脚本。该方法可用于全局 Worker 对象。这个方法会加载脚本并按照加载顺序同步执行。
// main.js
const worker = new Worker('./worker.js')
// importing scripts
// scriptA executes
// scriptB executes
// scripts imported
// scriptA.js
console.log('scriptA executes')
// scriptB.js
console.log('scriptB executes')
// worker.js
console.log('importing scripts')
importScripts('./scriptA.js')
importScripts('./scriptB.js')
console.log('scripts imported')importScripts()方法可以接收任意数量的脚本作为参数。浏览器下载它们的顺序没有限制,但执行则会严格按照它们在参数列表的顺序进行。因此,下面的代码与前面的效果一样:
console.log('importing scripts')
importScripts('./scriptA.js', './scriptB.js')
console.log('scripts imported')脚本加载受到常规 CORS 的限制,但在工作者线程内部可以请求来自任何源的脚本。这里的脚本导入策略类似于使用生成的<script>标签动态加载脚本。在这种情况下,所有导入的脚本也会共享作用域。
// main.js
const worker = new Worker('./worker.js', { name: 'foo' })
//importingscriptsinfoowithbar
//scriptAexecutesinfoowithbar
//scriptBexecutesinfoowithbar
// scripts imported
// scriptA.js
console.log(`scriptA executes in ${self.name} with ${globalToken}`)
// scriptB.js
console.log(`scriptB executes in ${self.name} with ${globalToken}`)
// worker.js
constglobalToken = 'bar'
console.log(`importing scripts in ${self.name} with ${globalToken}`)
importScripts('./scriptA.js', './scriptB.js')
console.log('scripts imported')27.2.7 委托任务到子工作者线程
有时候可能需要在工作者线程中再创建子工作者线程。在有多个 CPU 核心的时候,使用多个子工作者线程可以实现并行计算。使用多个子工作者线程前要考虑周全,确保并行计算的投入确实能够得到收益,毕竟同时运行多个子线程会有很大计算成本。
除了路径解析不同,创建子工作者线程与创建普通工作者线程是一样的。子工作者线程的脚本路径根据父工作者线程而不是相对于网页来解析。来看下面的例子(注意额外的 js 目录):
// main.js
const worker = new Worker('./js/worker.js')
// worker
// subworker
// js/worker.js
console.log('worker')
const worker = new Worker('./subworker.js')
// js/subworker.js
console.log('subworker')27.2.8 处理工作者线程错误
如果工作者线程脚本抛出了错误,该工作者线程沙盒可以阻止它打断父线程的执行。如下例所示,其中的 try/catch 块不会捕获到错误:
// main.js
try {
const worker = new Worker('./worker.js')
console.log('no error')
} catch (e) {
console.log('caught error')
}
// no error
// worker.js
throw Error('foo')不过,相应的错误事件仍然会冒泡到工作者线程的全局上下文,因此可以通过在 Worker 对象上设置错误事件侦听器访问到。下面看这个例子:
// main.js
const worker = new Worker('./worker.js')
worker.onerror = console.log
//ErrorEvent{message: "UncaughtError: foo"}
// worker.js
throw Error('foo')27.2.9 与专用工作者线程通信
与工作者线程的通信都是通过异步消息完成的,但这些消息可以有多种形式。
1.使用 postMessage()
最简单也最常用的形式是使用 postMessage()传递序列化的消息。下面来看一个计算阶乘的例子:
// factorialWorker.js
function factorial(n) {
let result = 1
while (n) {
result *= n--
}
return result
}
self.onmessage = ({ data }) => {
self.postMessage(`${data}! = ${factorial(data)}`)
}
// main.js
const factorialWorker = new Worker('./factorialWorker.js')
factorialWorker.onmessage = ({ data }) => console.log(data)
factorialWorker.postMessage(5)
factorialWorker.postMessage(7)
factorialWorker.postMessage(10)
// 5! = 120
// 7! = 5040
// 10! = 3628800对于传递简单的消息,使用 postMessage()在主线程和工作者线程之间传递消息,与在两个窗口间传递消息非常像。主要区别是没有 targetOrigin 的限制,该限制是针对 Window.prototype.postMessage 的,对 WorkerGlobalScope.prototype.postMessage 或 Worker.prototype.postMessage 没有影响。这样约定的原因很简单:工作者线程脚本的源被限制为主页的源,因此没有必要再去过滤了。
2.使用 MessageChannel
无论主线程还是工作者线程,通过 postMessage()进行通信涉及调用全局对象上的方法,并定义一个临时的传输协议。这个过程可以被 Channel Messaging API 取代,基于该 API 可以在两个上下文间明确建立通信渠道。
MessageChannel 实例有两个端口,分别代表两个通信端点。要让父页面和工作线程通过 MessageChannel 通信,需要把一个端口传到工作者线程中,如下所示:
worker.js
// 在监听器中存储全局messagePort
let messagePort = null
function factorial(n) {
let result = 1
while (n) {
result *= n--
}
return result
}
// 在全局对象上添加消息处理程序
self.onmessage = ({ ports }) => {
// 只设置一次端口
if (!messagePort) {
// 初始化消息发送端口,
// 给变量赋值并重置监听器
messagePort = ports[0]
self.onmessage = null
// 在全局对象上设置消息处理程序
messagePort.onmessage = ({ data }) => {
// 收到消息后发送数据
messagePort.postMessage(`${data}! = ${factorial(data)}`)
}
}
}
main.js
constchannel = newMessageChannel()
const factorialWorker = new Worker('./worker.js')
// 把`MessagePort`对象发送到工作者线程
// 工作者线程负责处理初始化信道
factorialWorker.postMessage(null, [channel.port1])
// 通过信道实际发送数据
channel.port2.onmessage = ({ data }) => console.log(data)
// 工作者线程通过信道响应
channel.port2.postMessage(5)
// 5! = 120在这个例子中,父页面通过 postMessage 与工作者线程共享 MessagePort。使用数组语法是为了在两个上下文间传递可转移对象。本章稍后会介绍可转移对象(Transferable)。工作者线程维护着对该端口的引用,并使用它代替通过全局对象传递消息。当然,消息的格式也需要临时约定:工作者线程收到的第一条消息包含端口,后续的消息才是数据。
使用 MessageChannel 实例与父页面通信很大程度上是多余的。这是因为全局 postMessage()方法本质上与 channel.postMessage()执行的是同样的操作(不考虑 MessageChannel 接口的其他特性)。MessageChannel 真正有用的地方是让两个工作者线程之间直接通信。这可以通过把端口传给另一个工作者线程实现。下面的例子把一个数组传给了一个工作者线程,这个线程又把它传另一个工作者线程,然后再传回主页:
main.js
const channel = new MessageChannel()
const workerA = new Worker('./worker.js')
const workerB = new Worker('./worker.js')
workerA.postMessage('workerA', [channel.port1])
workerB.postMessage('workerB', [channel.port2])
workerA.onmessage = ({ data }) => console.log(data)
workerB.onmessage = ({ data }) => console.log(data)
workerA.postMessage(['page'])
//['page', 'workerA', 'workerB']
workerB.postMessage(['page'])
//['page', 'workerB', 'workerA']
worker.js
let messagePort = null
let contextIdentifier = null
function addContextAndSend(data, destination) {
// 添加标识符以标识当前工作者线程
data.push(contextIdentifier)
// 把数据发送到下一个目标
destination.postMessage(data)
}
self.onmessage = ({ data, ports }) => {
// 如果消息里存在端口(ports)
// 则初始化工作者线程
if (ports.length) {
// 记录标识符
contextIdentifier = data
// 获取MessagePort
messagePort = ports[0]
// 添加处理程序把接收的数据
// 发回到父页面
messagePort.onmessage = ({ data }) => {
addContextAndSend(data, self)
}
} else {
addContextAndSend(data, messagePort)
}
}在这个例子中,数组的每一段旅程都会添加一个字符串,标识自己到过哪里。数组从父页面发送到工作者线程,工作者线程会加上自己的上下文标识符。然后,数组又从一个工作者线程发送到另一个工作者线程。第二个线程又加上自己的上下文标识符,随即将数组发回主页,主页把数组打印出来。这个例子中的两个工作者线程使用了同一个脚本,因此要注意数组可以双向传递。
3.使用 BroadcastChannel
同源脚本能够通过 BroadcastChannel 相互之间发送和接收消息。这种通道类型的设置比较简单,不需要像 MessageChannel 那样转移乱糟糟的端口。这可以通过以下方式实现:
main.js
constchannel = newBroadcastChannel('worker_channel')
const worker = new Worker('./worker.js')
channel.onmessage = ({ data }) => {
console.log(`heard ${data} on page`)
}
setTimeout(() => channel.postMessage('foo'), 1000)
//heardfooinworker
//heardbaronpage
worker.js
const channel = new BroadcastChannel('worker_channel')
channel.onmessage = ({ data }) => {
console.log(`heard ${data} in worker`)
channel.postMessage('bar')
}这里,页面在通过 BroadcastChannel 发送消息之前会先等 1 秒钟。因为这种信道没有端口所有权的概念,所以如果没有实体监听这个信道,广播的消息就不会有人处理。在这种情况下,如果没有 setTimeout(),则由于初始化工作者线程的延迟,就会导致消息已经发送了,但工作者线程上的消息处理程序还没有就位。
27.2.10 工作者线程数据传输
使用工作者线程时,经常需要为它们提供某种形式的数据负载。工作者线程是独立的上下文,因此在上下文之间传输数据就会产生消耗。在支持传统多线程模型的语言中,可以使用锁、互斥量,以及 volatile 变量。在 JavaScript 中,有三种在上下文间转移信息的方式:结构化克隆算法(structured clone algorithm)、可转移对象(transferable objects)和共享数组缓冲区(shared array buffers)。