工作者线程

工作者线程与线程

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()传递对象时,浏览器会遍历该对象,并在目标上下文中生成它的一个副本。

可转移对象可以把所有权从一个上下文转移到另一个上下文。只有如下几种对象是可转移对象:ArrayBufferMessagePortImageBitmapOffscreenCanvas

线程池

<!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属性中。