跳到主要内容

工作者线程

前端开发者常说:“JavaScript 是单线程的。”这种说法虽然有些简单,但描述了 JavaScript 在浏览器中的一般行为。因此,作为帮助 Web 开发人员理解 JavaScript 的教学工具,它非常有用。

单线程就意味着不能像多线程语言那样把工作委托给独立的线程或进程去做。JavaScript 的单线程可以保证它与不同浏览器 API 兼容。假如 JavaScript 可以多线程执行并发更改,那么像 DOM 这样的 API 就会出现问题。因此,POSIX 线程或 Java 的 Thread 类等传统并发结构都不适合 JavaScript。

而这也正是工作者线程的价值所在:允许把主线程的工作转嫁给独立的实体,而不会改变现有的单线程模型。虽然本章要介绍的各种工作者线程有不同的形式和功能,但它们的共同的特点是都独立于 JavaScript 的主执行环境。

工作者线程简介

JavaScript 环境实际上是运行在托管操作系统中的虚拟环境。在浏览器中每打开一个页面,就会分配一个它自己的环境。这样,每个页面都有自己的内存、事件循环、DOM,等等。每个页面就相当于一个沙盒,不会干扰其他页面。对于浏览器来说,同时管理多个环境是非常简单的,因为所有这些环境都是并行执行的。

使用工作者线程,浏览器可以在原始页面环境之外再分配一个完全独立的二级子环境。这个子环境不能与依赖单线程交互的 API(如 DOM)互操作,但可以与父环境并行执行代码。

工作者线程与线程

作为介绍,通常需要将工作者线程与执行线程进行比较。在许多方面,这是一个恰当的比较,因为工作者线程和线程确实有很多共同之处。

工作者线程是以实际线程实现的
例如,Blink浏览器引擎实现工作者线程的WorkerThread就对应着底层的线程。
工作者线程并行执行
虽然页面和工作者线程都是单线程 JavaScript 环境,每个环境中的指令则可以并行执行。
工作者线程可以共享某些内存
工作者线程能够使用 SharedArrayBuffer 在多个环境间共享内容。虽然线程会使用锁实现并发控制,但 JavaScript 使用 Atomics 接口实现并发控制。 工作者线程与线程有很多类似之处,但也有重要的区别。
工作者线程不共享全部内存
在传统线程模型中,多线程有能力读写共享内存空间。除了 Shared- ArrayBuffer 外,从工作者线程进出的数据需要复制或转移。
工作者线程不一定在同一个进程里
通常,一个进程可以在内部产生多个线程。根据浏览器引擎的实现,工作者线程可能与页面属于同一进程,也可能不属于。例如,Chrome 的 Blink 引擎对共享工作者线程和服务工作者线程使用独立的进程。
创建工作者线程的开销更大
工作者线程有自己独立的事件循环、全局对象、事件处理程序和其他 JavaScript 环境必需的特性。创建这些结构的代价不容忽视无论形式还是功能,工作者线程都不是用于替代线程的。


HTML Web 工作者线程规范

工作者线程相对比较重,不建议大量使用。例如,对一张 400 万像素的图片,为每个像素都启动一个工作者线程是不合适的。通常,工作者线程应该是长期运行的,启动成本比较高, 每个实例占用的内存也比较大。

工作者线程的类型

Web 工作者线程规范中定义了三种主要的工作者线程:专用工作者线程共享工作者线程服务工作者线程。现代浏览器都支持这些工作者线程。

  1. 专用工作者线程

专用工作者线程,通常简称为工作者线程、Web Worker 或 Worker,是一种实用的工具,可以让脚本单独创建一个 JavaScript 线程,以执行委托的任务。专用工作者线程,顾名思义,只能被创建它的页面使用。

  1. 共享工作者线程

共享工作者线程与专用工作者线程非常相似。主要区别是共享工作者线程可以被多个不同的上下文使用,包括不同的页面。任何与创建共享工作者线程的脚本同源的脚本,都可以向共享工作者线程发送消息或从中接收消息。

  1. 服务工作者线程

服务工作者线程与专用工作者线程和共享工作者线程截然不同。它的主要用途是拦截、重定向和修改页面发出的请求,充当网络请求的仲裁者的角色。

WorkerGlobalScope

在网页上,window 对象可以向运行在其中的脚本暴露各种全局变量。在工作者线程内部,没有 window 的概念。这里的全局对象是 WorkerGlobalScope 的实例,通过 self 关键字暴露出来。如下是 WorkerGlobalScope 属性和方法:

self 上可用的属性是 window 对象上属性的严格子集。其中有些属性会返回特定于工作者线程的版本。

属性/方法类型描述
self属性指向自身的引用,可以用于在 worker 脚本中访问 WorkerGlobalScope 对象。
location属性返回 WorkerLocation 对象,表示工作者上下文的地址信息。
navigator属性返回 WorkerNavigator 对象,提供关于工作者的代理、语言和其他上下文信息。
onerror属性一个处理错误事件的事件处理程序,当 worker 中发生错误时触发。
onlanguagechange属性当用户的语言设置改变时触发的事件处理程序。
onoffline属性当 worker 从在线状态转为离线状态时触发的事件处理程序。
ononline属性当 worker 从离线状态转为在线状态时触发的事件处理程序。
onrejectionhandled属性当 worker 处理了一个先前被拒绝的 promise 时触发的事件处理程序。
onunhandledrejection属性当 worker 中的一个 promise 被拒绝且没有处理程序时触发的事件处理程序。
importScripts()方法用于动态导入脚本到 worker 中。
setTimeout()方法在指定的时间后执行一个函数或指定的代码片段。
clearTimeout()方法清除由 setTimeout() 设置的定时器。
setInterval()方法在指定的时间间隔内重复执行一个函数或代码片段。
clearInterval()方法清除由 setInterval() 设置的定时器。
fetch()方法用于发起 HTTP 请求并返回包含响应的 promise。
atob()方法解码一个 Base64 编码的字符串。
btoa()方法创建一个字符串的 Base64 编码版本。
addEventListener()方法添加事件监听器来处理特定事件类型。
removeEventListener()方法移除使用 addEventListener() 添加的事件监听器。
dispatchEvent()方法触发指定事件,调用绑定到事件的事件处理程序。

WorkerGlobalScope 的子类

实际上并不是所有地方都实现了 WorkerGlobalScope。每种类型的工作者线程都使用了自己特定的全局对象,这继承自 WorkerGlobalScope。

  • 专用工作者线程使用 DedicatedWorkerGlobalScope。
  • 共享工作者线程使用 SharedWorkerGlobalScope。
  • 服务工作者线程使用 ServiceWorkerGlobalScope。

专用工作者线程

专用工作者线程是最简单的 Web 工作者线程,网页中的脚本可以创建专用工作者线程来执行在页面线程之外的其他任务。这样的线程可以与父页面交换信息、发送网络请求、执行文件输入/输出、进行密集计算、处理大量数据,以及实现其他不适合在页面执行线程里做的任务(否则会导致页面响应迟钝)。

基本概念

可以把专用工作者线程称为后台脚本(background script)。JavaScript 线程的各个方面,包括生命周期管理、代码路径和输入/输出,都由初始化线程时提供的脚本来控制。该脚本也可以再请求其他脚本, 但一个线程总是从一个脚本源开始。

创建专用工作者线程

创建专用工作者线程最常见的方式是加载 JavaScript 文件。把文件路径提供给 Worker 构造函数, 然后构造函数再在后台异步加载脚本并实例化工作者线程。传给构造函数的文件路径可以是多种形式。

main.js引入worker.js示例
// worker.js 与 main.js 同一目录下
const worker = new Worker('worker.js');

工作者线程本身存在于一个独立的 JavaScript 环境中,因此 main.js 必须以 Worker 对象为代理实现与工作者线程通信。

工作者线程安全限制

工作者线程的脚本文件只能从与父页面相同的源加载。从其他源加载工作者线程的脚本文件会导致错误。

// 尝试基于 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: Uncaught DOMException: Failed to construct 'Worker':
// Script at https://untrusted.com/main.js cannot be accessed
// from origin https://example.com

基于加载脚本创建的工作者线程不受文档的内容安全策略限制,因为工作者线程在与父文档不同的上下文中运行。不过,如果工作者线程加载的脚本带有全局唯一标识符(与加载自一个二进制大文件一样),就会受父文档内容安全策略的限制。

使用 Worker 对象

Worker() 构造函数返回的 Worker 对象是与刚创建的专用工作者线程通信的连接点。它可用于在工作者线程和父上下文间传输信息,以及捕获专用工作者线程发出的事件。

信息

要管理好使用 Worker() 创建的每个 Worker 对象。在终止工作者线程之前,它不会被垃圾回收,也不能通过编程方式恢复对之前 Worker 对象的引用。

Worker 对象支持下列事件处理程序属性:

属性/方法说明
onerror在工作者线程中发生 ErrorEvent 类型的错误事件时会调用指定给该属性的处理程序。可以通过 worker.onerrorworker.addEventListener('error', handler) 的形式处理。
onmessage当工作者线程向主线程发送消息时触发。可以通过 worker.onmessageworker.addEventListener('message', handler) 处理。
onmessageerror当工作者线程收到无法反序列化的消息时触发。可以通过 worker.onmessageerrorworker.addEventListener('messageerror', handler) 处理。
postMessage()用于向工作者线程发送信息,主线程与工作者线程之间的异步通信机制。
terminate()立即终止工作者线程,不提供清理资源的机会。脚本会立刻停止执行。

DedicatedWorkerGlobalScope

在专用工作者线程内部,全局作用域是 DedicatedWorkerGlobalScope 的实例。因为这继承自 WorkerGlobalScope,所以包含它的所有属性和方法。工作者线程可以通过 self 关键字访问该全局作用域。

顶级脚本和工作者线程中的 console 对象都将写入浏览器控制台,这对于调试非常有用。因为工作者线程具有不可忽略的启动延迟,所以即使 Worker 对象存在,工作者线程的日志也会在主线程的日志之后打印出来。

DedicatedWorkerGlobalScope 在 WorkerGlobalScope 基础上增加了以下属性和方法。

属性/方法说明
name可以提供给 Worker 构造函数的一个可选的字符串标识符。
postMessage()worker.postMessage() 对应的方法,用于从工作者线程内部向父上下文发送消息。
close()worker.terminate() 对应的方法,用于立即终止工作者线程。没有为工作者线程提供清理的机会,脚本会突然停止。
importScripts()用于向工作者线程中导入任意数量的脚本。

专用工作者线程与隐式 MessagePorts

专用工作者线程的 Worker 对象和 DedicatedWorkerGlobalScope 与 MessagePorts 有一些相同接口处理程􏰀和方法:onmessageonmessageerrorclose()postMessage()。这不是偶然的,因为专用工作者线程隐式使用了 MessagePorts 在两个上下文之间通信。

父上下文中的 Worker 对象和 DedicatedWorkerGlobalScope 实际上融合了 MessagePort,并在自己的接口中分别暴露了相应的处理程序和方法。换句话说,消息还是通过 MessagePort 发送,只是没有直接使用 MessagePort 而已。

也有不一致的地方,比如 start()close() 约定。专用工作者线程会自动发送排队的消息,因此 start() 也就没有必要了。另外,close() 在专用工作者线程的上下文中没有意义,因为这样关闭 MessagePort 会使工作者线程孤立。因此,在工作者线程内部调用 close() (或在外部调用 terminate()) 不仅会关闭 MessagePort,也会终止线程。

专用工作者线程的生命周期

调用 Worker() 构造函数是一个专用工作者线程生命的起点。调用之后,它会初始化对工作者线程脚本的请求,并把 Worker 对象返回给父上下文。虽然父上下文中可以立即使用这个 Worker 对象,但与之关联的工作者线程可能还没有创建,因为存在请求脚本的网格延迟和初始化延迟。

一般来说,专用工作者线程可以非正式区分为处于下列三个状态:初始化(initializing)活动(active)终止(terminated)。这几个状态对其他上下文是不可见的。虽然 Worker 对象可能会存在于父上下文中,但也无法通过它确定工作者线程当前是处理初始化、活动还是终止状态。换句话说,与活动的专用工作者线程关联的 Worker 对象和与终止的专用工作者线程关联的 Worker 对象无法分别。

初始化时,虽然工作者线程脚本尚未执行,但可以先把要发送给工作者线程的消息加入队列。这些消息会等待工作者线程的状态变为活动,再把消息添加到它的消息队列。

示例
// worker.js
self.addEventListener('message', ({ data }) => console.log(data));

// main.js
const worker = new Worker('./worker.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
worker.onmessage = ({ data }) => console.log(data);
const worker = new Worker('./closeWorker.js');

// foo
// bar

/*
虽然调用了 close(),但显然工作者线程的执行并没有立即终止。
close()在这里会通知工作者线程取消事件循环中的所有任务,并阻止继续添加新任务。这也是为什么"baz"没有打印出来的原因。
工作者线程不需要执行同步停止,因此在父上下文的事件循环中处理的"bar"仍会打印出来。
*/
信息

close()terminate() 是幂等操作,多次调用没有问题。这两个方法仅仅是将 Worker 标记为 teardown,因此多次调用不会有不好的影响。

在整个生命周期中,一个专用工作者线程只会关联一个网页(Web 工作者线程规范称其为一个文档)。 除非明确终止,否则只要关联文档存在,专用工作者线程就会存在。如果浏览器离开网页(通过导航或关闭标签页或关闭窗口),它会将与其关联的工作者线程标记为终止,它们的执行也会立即停止。

配置 Worker 选项

Worker() 构造函数允许将可选的配置对象作为第二个参数。该配置对象支持下列属性。

属性描述
name可以在工作者线程中通过 self.name 读取到的字符串标识符。
type表示加载脚本的运行方式,可以是"classic"或"module"。"classic"将脚本作为常规脚本来执行,"module"将脚本作为模块来执行。
credentials在 type 为"module"时,指定如何获取与传输凭证数据相关的工作者线程模块脚本。值可以是"omit"、"same-orign"或"include"。这些选项与 fetch() 的凭证选项相同。 在 type 为"classic"时,默认为"omit"。

在 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() 构造函数。该构造函数同样创建了专用工作者线程。

代码浓缩示例
const worker = new Worker(
URL.createObjectURL(
new Blob([`self.onmessage = ({data}) => console.log(data);`]),
),
);

worker.postMessage('blob worker script');
// blob worker script

工作者线程也可以利用函数􏰀列化来初始化行内脚本。这是因为函数的 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);
// 34

这里有意使用了斐波那契数列的实现,将其序列化之后传给了工作者线程。该函数作为 IIFE 调用并传递参数,结果则被发送回主线程。虽然计算斐波那契数列比较耗时,但所有计算都会委托到工作者线程,因此并不会影响父上下文的性能。

在工作者线程中动态执行脚本

工作者线程中的脚本并非铁板一块,而是可以使用 importScripts() 方法通过编程方式加载和执行任意脚本。该方法可用于全局 Worker 对象。这个方法会加载脚本并按照加载顺序同步执行。

// worker.js
importScripts('script1.js', 'script2.js');

// script1.js 和 script2.js 将按照顺序被加载并执行

importScripts() 方法可以接收任意数量的脚本作为参数。浏览器下载它们的顺􏰀没有限制,但执行则会严格按照它们在参数列表的顺序进行。

脚本加载受到常规 CORS 的限制,但在工作者线程内部可以请求来自任何源的脚本。这里的脚本导入策略类似于使用生成的 <script> 标签动态加载脚本。在这种情况下,所有导入的脚本也会共享作用域。

委托任务到子工作者线程

有时候可能需要在工作者线程中再创建子工作者线程。在有多个 CPU 核心的时候,使用多个子工作者线程可以实现并行计算。使用多个子工作者线程前要考虑周全,确保并行计算的投入确实能够得到收益,毕竟同时运行多个子线程会有很大计算成本。

除了路径解析不同,创建子工作者线程与创建􏰁通工作者线程是一样的。子工作者线程的脚本路径根据父工作者线程而不是相对于网页来解析。

注意

顶级工作者线程的脚本和子工作者线程的脚本都必须从与主页相同的源加载。

处理工作者线程错误

如果工作者线程脚本抛出了错误,该工作者线程沙盒可以阻止它打断父线程的执行。如下例所示,其中的 try/catch 块不会捕获到错误:

// worker.js
throw Error('foo');

// main.js
try {
const worker = new Worker('./worker.js');
console.log('no error');
} catch (e) {
console.log('caught error');
}

// no error

不过,相应的错误事件仍然会冒泡到工作者线程的全局上下文,因此可以通过在 Worker 对象上设置错误事件侦听器访问到。

const worker = new Worker('./worker.js');
worker.onerror = console.log;

与专用工作者线程通信

与工作者线程的通信都是通过异步消息完成的,但这些消息可以有多种形式。

使用 postMessage()

最简单也最常用的形式是使用 postMessage() 传递序列化的消息。

对于传递简单的消息,使用 postMessage() 在主线程和工作者线程之间传递消息,与在两个窗口间传递消息非常像。主要区别是没有 targetOrigin 的限制,该限制是针对 Window.prototype. postMessage 的,对 WorkerGlobalScope.prototype.postMessage 或 Worker.prototype. postMessage 没有影响。这样约定的原因很简单:工作者线程脚本的源被限制为主页的源,因此没有必要再去过滤了。

使用 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
const channel = new MessageChannel();
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

使用 BroadcastChannel

同源脚本能够通过 BroadcastChannel 相互之间发送和接收消息。这种通道类型的设置比较简单,不需要像 MessageChannel 那样转移乱糟糟的端口。这可以通过以下方式实现:

main.js
const channel = new BroadcastChannel('worker_channel');
const worker = new Worker('./worker.js');
channel.onmessage = ({ data }) => {
console.log(`heard ${data} on page`);
};
setTimeout(() => channel.postMessage('foo'), 1000);
// heard foo in worker
// heard bar on page
worker.js
const channel = new BroadcastChannel('worker_channel');

channel.onmessage = ({ data }) => {
console.log(`heard ${data} in worker`);
channel.postMessage('bar');
};

这里,页面在通过 BroadcastChannel 发送消息之前会先等 1 秒钟。因为这种信道没有端口所有权的概念,所以如果没有实体监听这个信道,广播的消息就不会有人处理。在这种情况下,如果没有 setTimeout(),则由于初始化工作者线程的延迟,就会导致消息已经发送了,但工作者线程上的消息处理程序还没有就位。

工作者线程数据传输

使用工作者线程时,经常需要为它们提供某种形式的数据负载。工作者线程是独立的上下文,因此在上下文之间传输数据就会产生消耗。在支持传统多线程模型的语言中,可以使用锁、互斥量,以及 volatile 变量。在 JavaScript 中,有三种在上下文间转移信息的方式:结构化克隆算法(structured clone algorithm)、可转移对象(transferable objects)和共享数组缓冲区(shared array buffers)。

结构化克隆算法

结构化克隆算法用于在两个独立上下文(如主线程与 Web Worker 线程)之间共享数据时生成数据的深拷贝。它由浏览器在后台实现,开发者无法直接调用。当通过 postMessage() 传递对象时,浏览器会使用结构化克隆算法遍历该对象,并在目标上下文中创建一个副本。

以下是结构化克隆算法支持的类型:

除 Symbol 之外的所有原始类型
Boolean 对象
String 对象
BDate
RegExp
Blob
File
FileList
ArrayBuffer
ArrayBufferView
ImageData
Array
Object
Map
Set


关于结构化克隆算法,有以下几点需要注意:

  • 复制之后,源上下文中对该对象的修改,不会传播到目标上下文中的对象。
  • 结构化克隆算法可以识别对象中包含的循环引用,不会无穷遍历对象。
  • 克隆 Error 对象、Function 对象或 DOM 节点会抛出错误。
  • 结构化克隆算法并不总是创建完全一致的副本。
  • 对象属性描述符、获取方法和设置方法不会克隆,必要时会使用默认值。
  • 原型链不会克隆。
  • RegExp.prototype.lastIndex 属性不会克隆。

可转移对象

使用可转移对象(transferable objects)可以把所有权从一个上下文转移到另一个上下文。在不太可 能在上下文间复制大量数据的情况下,这个功能特别有用。只有如下几种对象是可转移对象:

  • ArrayBuffer
  • MessagePort
  • ImageBitmap
  • OffscreenCanvas

postMessage() 方法的第二个可选参数是数组,它指定应该将哪些对象转移到目标上下文。在遍 历消息负载对象时,浏览器根据转移对象数组检查对象引用,并对转移对象进行转移而不复制它们。这 意味着被转移的对象可以通过消息负载发送,消息负载本身会被复制,比如对象或数组。

SharedArrayBuffer

既不克隆,也不转移,SharedArrayBuffer 作为 ArrayBuffer 能够在不同浏览器上下文间共享。在把 SharedArrayBuffer 传给 postMessage() 时,浏览器只会传递原始缓冲区的引用。结果是,两 个不同的 JavaScript 上下文会分别维护对同一个内存块的引用。每个上下文都可以随意修改这个缓冲区, 就跟修改常规 ArrayBuffer 一样。

线程池

因为启用工作者线程代价很大,所以某些情况下可以考虑始终保持固定数量的线程活动,需要时就 把任务分派给它们。工作者线程在执行计算时,会被标记为忙碌状态。直到它通知线程池自己空闲了, 才准备好接收新任务。这些活动线程就称为“线程池”或“工作者线程池”。

线程池中线程的数量多少合适并没有权威的答案,不过可以参考 navigator.hardware Concurrency 属性返回的系统可用的核心数量。因为不太可能知道每个核心的多线程能力,所以最好把这个数字作为 线程池大小的上限。

一种使用线程池的策略是每个线程都执行同样的任务,但具体执行什么任务由几个参数来控制。通 过使用特定于任务的线程池,可以分配固定数量的工作者线程,并根据需要为他们提供参数。工作者线 程会接收这些参数,执行耗时的计算,并把结果返回给线程池。然后线程池可以再将其他工作分派给工 作者线程去执行。接下来的例子将构建一个相对简单的线程池,但可以涵盖上述思路的所有基本要求。

共享工作者线程

服务工作者线程