工作者线程
工作者线程与线程
1、工作者线程是以实际线程实现的。
2、工作者线程并行执行。
3、工作者线程可以共享某些内存。工作者线程能够使用SharedArrayBuffer
再多个环境间共享内容。虽然线程会使用锁实现并发控制,JS使用Atomics
接口实现并发控制
4、工作者线程不共享全部内存。
5、工作者线程不一定在同一个进程里。
6、创建工作者线程的开销更大。
工作者线程的类型
1、专用工作者线程
Web Worker
,只能被创建它的页面使用。
2、共享工作者线程
共享工作者线程可以被多个不同的上下文使用,包括不同的页面。任何与创建共享工作者线程的脚本同源的脚本,都可以共享工作者线程发送消息或从中接收消息。
3、服务工作者线程
主要用途是拦截、重定向和修改页面发出的请求,充当网络请求的仲裁者的角色。
WorkerGlobalScope
在工作者线程内部,没有window概念。全局对象是WorkerGlobalScope
的实例,通过self
关键字暴露出来。
并不是所有地方都实现了WorkerGlobalScope
,每种类型的工作者线程都使用了自己特定的全局对象,继承自WorkerGlobalScope
。
专用工作者线程使用DedicatedWorkerGlobalScope
共享工作者线程使用SharedWorkerGlobalScope
服务工作者线程使用ServiceWorkerGlobalScope
专用工作者线程
创建专用工作者线程
console.log(location.href);
const worker = new Worker(location.href+'emptyWorker.js');
console.log(worker);
安全限制
工作者线程的脚本文件只能从与父页面相同的源加载。从其它源加载工作者线程的脚本文件会导致错误。
使用worker对象
worker()
构造函数返回的Worker
对象是刚创建的专用工作者线程通信的连接点。
DedicatedWorkerGlobalScope
,全局作用域。通过self
关键字访问。
工作者线程的生命周期
调用close()
工作者线程的执行没有立即终止。
close()
会通知工作者线程取消事件循环中的所有任务,并阻止继续添加新任务。工作者线程不需要执行同步停止。
调用terminate()
,工作者线程的消息队列会被清理并锁住。
close()
和terminate()
是幂等操作,多次调用没有问题。方法仅仅是将worker标记为teardown
在工作者线程中动态执行脚本
importScripts("./scriptA.js")
importScripts()
方法可以接受任意数量多脚本作为参数。执行会严格按照它们在参数列表的顺序执行。
与专用工作者线程通信
//emptyworker.js
self.onmessage=({data})=>{
self.postMessage(`${data}`);
}
//main.js
const worker = new Worker('./emptyWorker.js');
worker.onmessage=({data})=>console.log(data);
worker.postMessage('foo')
worker.postMessage('bar')
worker.postMessage('baz')
与在两个窗口间传递消息非常像。主要区别是没有targetOrigin
的限制,该限制是针对window.prototype.postMessage
的。
工作者线程脚本的源被限制为主页的源,因此没有必要再去过滤了。
使用MessageChannel
无论主线程还是工作者线程,通过postMessage()
进行通信涉及调用全局对象上的方法,并定义一个临时的传输协议。过程可以被Channel Messaging API取代。
MessageChannel实例有两个端口,分别代表两个通信端点。要让父页面和工作线程通过MessageChannel通信,需要把一个端口传到工作者线程中。
//main.js
//MessageChannel
const channel = new MessageChannel();
const factorialWorker = new Worker('./worker.html')
//把`MessagePort`对象发送到工作者线程
//工作者线程负责处理初始化信道
factorialWorker.postMessage(null,[channel.port1]);
//通过信道实际发送数据
channel.port2.onmessage=({data})=>console.log(data)
//工作者线程通过信道响应
channel.port2.postMessage(5);
//worker.js
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)}`);
}
}
}
使用BroadcastChannel
同源脚本能够通过BroadcastChannel
相互之间发送和接受消息。
//main.js
const channel = new BroadcastChannel('worker_channel');
const worker = new Worker('./worker.js')
channel.onmessage=({data})=>console.log(data)
setTimeout(()=>channel.postMessage('foo'),1000);
//worker.js
const channel = new BroadcastChannel('worker_channel');
channel.onmessage=({data})=>{
console.log(``heard${data} in worker);
channel.postMessage("bar");
}
工作者线程数据传输
在JS中有三种在上下文之间转移信息的方式:结构化克隆算法、可转移对象和共享数组缓冲区
结构化克隆算法可用于在两个独立上下文间共享数据。该算法由浏览器实现,不能直接调用。
通过postMessage()
传递对象时,浏览器会遍历该对象,并在目标上下文中生成它的一个副本。
可转移对象可以把所有权从一个上下文转移到另一个上下文。只有如下几种对象是可转移对象:ArrayBuffer
、MessagePort
、ImageBitmap
和OffscreenCanvas
线程池
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<script>
/*
* 负责两件事:跟踪线程是否正忙于工作,并管理进出线程的信息与事件
* 传入给这个工作者线程的任务会封装到一个期约中,然后正确地解决和拒绝。
* */
class TaskWorker extends Worker{
constructor(notifyAvailable,...workerArgs) {
super(...workerArgs);
//初始化为不可用状态
this.available =false;
this.resolve=null;
this.reject=null;
//线程池会传递回调
//以便工作者线程发出它需要新任务的信号
this.notifyAvailable = notifyAvailable;
//线程脚本在完全初始化之后
//会发送一条"ready"消息
this.onmessage = ()=>this.setAvailable();
}
//由线程池调用,以分派新任务
dispatch({resolve,reject,postMessageArgs}){
this.available=false;
this.onmessage=({data})=>{
resolve(data);
this.setAvailable();
};
this.onerror=(e)=>{
reject(e);
this.setAvailable();
};
this.postMessage(...postMessageArgs);
}
setAvailable(){
this.available=true;
this.resolve=null;
this.reject=null;
this.notifyAvailable();
}
}
//定义使用TaskWorker类的WorkerPool类
//必须维护尚未分派给工作者线程的任务队列
class WorkerPool{
constructor(poolSize,...workerArgs) {
this.taskQueue=[];
this.workers=[];
//初始化线程池
for(let i=0;i<poolSize;++i){
this.workers.push(
new TaskWorker(()=>this.dispatchIfAvailable(),...workerArgs)
)
}
}
//把任务推入队列
enqueue(...postMessageArgs){
return new Promise((resolve,reject)=>{
this.taskQueue.push({resolve,reject,postMessageArgs})
this.dispatchIfAvailable();
})
}
dispatchIfAvailable(){
if(!this.taskQueue.length){
return;
}
for (const worker of this.workers){
if(worker.available){
let a = this.taskQueue.shift();
worker.dispatch(a);
break;
}
}
}
//终止所有工作者线程
close(){
for(const worker of this.workers){
worker.terminate();
}
}
}
</script>
</body>
</html>
Worker.js
//工作脚本,创建一千万个节点
self.onmessage=({data})=>{
let sum=0;
let view = new Float32Array(data.arrayBuffer)
//求和
for(let i=data.startIdx;i<data.endIdx;++i){
sum+=view[i];
}
self.postMessage(sum);
};
self.postMessage('ready');
Main.js
const totalFloats = 1E8;
const numTasks = 20;
const floatPerTask = totalFloats/numTasks;
const numWokers=4;
//创建线程池
const pool = new WorkerPool(numWokers,'./worker.js');
//填充浮点值数组
let arrayBuffer = new SharedArrayBuffer(4*totalFloats);
let view = new Float32Array(arrayBuffer);
for(let i=0;i<totalFloats;++i){
view[i]=Math.random();
}
let partialSumPromises=[]
for(let i=0;i<totalFloats;i+=floatPerTask){
partialSumPromises.push(
pool.enqueue({
startIdx:i,
endIdx:i+floatPerTask,
arrayBuffer:arrayBuffer
})
);
}
//等待所有期约完成,然后求和
Promise.all(partialSumPromises)
.then((partialSums)=>partialSums.reduce((x,y)=>x+y))
.then(console.log);
共享工作者线程
创建共享工作者线程
emptySharedWorker.js
空文件
Main.js
console.log(location.href)
const sharedWorker = new SharedWorker(
location.href + 'emptySharedWorker.js'
)
console.log(sharedWorker)
共享工作者线程于专用工作者线程的一个重要区别在于,虽然Worker()
构造函数始终会创建新实例,而SharedWorker()
则只会在相同的标识不存在的情况下才创建新实例。如果存在与标识匹配的共享工作者线程,则只会与已有共享工作者线程建立新的连接。
//此段脚本实例话一个共享工作者线程并添加两个连接
new SharedWorker('./emptySharedWorker.js')
new SharedWorker('./emptySharedWorker.js')
new SharedWorker('./emptySharedWorker.js')
理解共享工作者线程的生命周期
专用工作者线程只跟一个页面绑定,而共享工作者线程只要还有一个上下文连接就会持续存在。
关键在于,没有办法以编程方式终止共享线程。
连接到共享工作者线程
SharedWorker.js
let i=0;
self.onconnect = ()=> console.log(`connected ${++i} times`)
Main.js
for(let i = 0; i <5;i++){
new SharedWorker('./sharedWorker.js')
}
服务工作者线程
是浏览器中代理服务器的线程,可以拦截外出请求和缓存响应。
服务工作者线程在两个主要任务上最有用:充当网络请求的缓存层和启用推送通知。
创建服务工作者线程
服务工作者线程没有全局构造函数,实例保存在navigator.serviceWorker
属性中。
- 本文链接:https://archer-lan.github.io/2023/11/20/%E5%B7%A5%E4%BD%9C%E8%80%85%E7%BA%BF%E7%A8%8B/
- 版权声明:本博客所有文章除特别声明外,均默认采用 许可协议。