聊聊 webworker

7,445 阅读9分钟

先看几个例子

本例子是通过通过红点展示地球上的地震带,数据来自于地质探测局
通过console.log看到数据运算所耗的时间
不使用 webworker No web workers - all on main thread
使用一条 webworker One web worker
使用两条 Two web workers
使用八条 Eight web workers
使用20条 20 web workers

结论:是? // 带着思考看下去

背景

JavaScript引擎是单线程运行的,JavaScript中耗时的I/O操作都被处理为异步操作,它们包括键盘、鼠标I/O输入输出事件、窗口大小的resize事件、定时器(setTimeout、setInterval)事件、Ajax请求网络I/O回调等。当这些异步任务发生的时候,它们将会被放入浏览器的事件任务队列中去,等到JavaScript运行时执行线程空闲时候才会按照队列先进先出的原则被一一执行,但终究还是单线程。

虽然JS运行在浏览器中,是单线程的,每个window一个JS线程,但浏览器不是单线程的,例如Webkit或是Gecko引擎,都可能有如下线程:

javascript引擎线程
界面渲染线程
浏览器事件触发线程
Http请求线程

很多人觉得异步(promise async/await),都是通过类似event loop在平常的工作中已经足够,但是如果做复杂运算,这些异步伪线程的不足就逐渐体现出来,比如settimeout拿到的值并不正确,再者假如页面有复杂运算的时候页面很容易触发假死状态,
为了有多线程功能,webworker问世了。不过,这并不意味着 JavaScript 语言本身就支持了多线程,对于 JavaScript 语言本身它仍是运行在单线程上的, Web Worker 只是浏览器(宿主环境)提供的一个能力/API

简介

Web Worker 是HTML5标准的一部分,这一规范定义了一套 API,它允许一段JavaScript程序运行在主线程之外的另外一个线程中。工作线程允许开发人员编写能够长时间运行而不被用户所中断的后台程序, 去执行事务或者逻辑,并同时保证页面对用户的及时响应,可以将一些大量计算的代码交给web worker运行而不冻结用户界面,后面会有案例介绍

类型

Web workers可分为两种类型:专用线程dedicated web worker,以及共享线程shared web workerDedicated web worker随当前页面的关闭而结束;这意味着Dedicated web worker只能被创建它的页面访问。与之相对应的Shared web worker可以被多个页面访问。在Javascript代码中,“Work”类型代表Dedicated web worker,而“SharedWorker”类型代表Shared web worker
Shared Worker则可以被多个页面所共享(同域情况下)

如何创建

Web Worker的创建是在主线程当中通过传入文件的url来实现的。如下所示:

let webworker = new Worker('myworker.js');

返回的是webworker实例对象,该对象是主线程和其他线程的通讯桥梁
主线程和其他线程可以通过

onmessage: 监听事件
postmessage: 传送事件

相关的API进行通讯
案例代码如下

//主线程 main.js
var worker = new Worker("worker.js");
worker.onmessage = function(event){
    // 主线程收到子线程的消息
};
// 主线程向子线程发送消息
worker.postMessage({
    type: "start",
    value: 12345
});

//web worker.js
onmessage = function(event){
   // 收到
};
postMessage({
    type: "debug",
    message: "Starting processing..."
});

相关demo

如何终止

如果在某个时机不想要 Worker 继续运行了,那么我们需要终止掉这个线程,可以调用 在主线程workerterminate 方法 或者在相应的线程中调用close

// 方式一 main.js 在主线程停止方式 
var worker = new Worker('./worker.js');
...
worker.terminate();

// 方式二、worker.js
self.close()

错误机制

提供了onerror API

worker.addEventListener('error', function (e) {
  console.log('MAIN: ', 'ERROR', e);
  console.log('filename:' + e.filename + '-message:' + e.message + '-lineno:' + e.lineno);
});

// event.filename: 导致错误的 Worker 脚本的名称;
// event.message: 错误的信息;
// event.lineno: 出现错误的行号;

sharedWorker

对于 Web Worker ,一个 tab 页面只能对应一个 Worker 线程,是相互独立的;
SharedWorker 提供了能力能够让不同标签中页面共享的同一个 Worker 脚本线程;
当然,有个很重要的限制就是它们需要满足同源策略,也就是需要在同域下;
在页面(可以多个)中实例化 Worker 线程:

// main.js

var myWorker = new SharedWorker("worker.js");

myWorker.port.start();

myWorker.port.postMessage("hello, I'm main");

myWorker.port.onmessage = function(e) {
  console.log('Message received from worker');
}
// worker.js
onconnect = function(e) {
  var port = e.ports[0];

  port.addEventListener('message', function(e) {
    var workerResult = 'Result: ' + (e.data[0]);
    port.postMessage(workerResult);
  });
  port.start();
}

在线demo

父子线程

线程中再创建线程

环境与作用域

Worker 线程的运行环境中没有 window 全局对象,也无法访问 DOM 对象,所以一般来说他只能来执行纯 JavaScript 的计算操作。但是,他还是可以获取到部分浏览器提供的 API 的:

setTimeout(), clearTimeout(), setInterval(), clearInterval():有了设计个函数,就可以在 Worker : 线程中可以再创建worker;
XMLHttpRequest : 对象:意味着我们可以在 Worker 线程中执行 ajax 请求;
navigator 对象:可以获取到 ppName,appVersion,platform,userAgent 等信息;
location 对象(只读):可以获取到有关当前 URL 的信息;
Application Cache
indexedDB
WebSocket、
Promise、

库或外部脚本引入和访问

在线程中,提供了importScripts方法
如果线程中使用了importScripts 一般按照以下步骤解析

1、解析 importScripts方法的每一个参数。
2、如果有任何失败或者错误,抛出 SYNTAX_ERR 异常。
3、尝试从用户提供的 URL 资源位置处获取脚本资源。
4、对于 importScripts 方法的每一个参数,按照用户的提供顺序,获取脚本资源后继续进行其它操作。
// worker.js
importScripts('math_utilities.js'); 
onmessage = function (event) 
 { 
     var first=event.data.first; 
     var second=event.data.second; 
     calculate(first,second); // calculate 是math_utilities.js中的方法 
 }; 

也可以一次性引入多个

//可以多起一次传入
importScripts('script1.js', 'script2.js');

XMLHttpRequest

onmessage = function(evt){
    var xhr = new XMLHttpRequest();
    xhr.open("GET", "serviceUrl"); //serviceUrl为后端j返回son数据的接口
    xhr.onload = function(){
    postMessage(xhr.responseText);
    };
    xhr.send();
}
jsonp
// 设置jsonp
function MakeServerRequest() 
{
    importScripts("http://SomeServer.com?jsonp=HandleRequest");
} 

// jsonp回调
function HandleRequest(objJSON) 
{
    postMessage("Data returned from the server...FirstName: " 
                  + objJSON.FirstName + " LastName: " + objJSON.LastName);
} 

// Trigger the server request for the JSONP data 
MakeServerRequest();

通讯原理

从一个线程到另一个线程的通讯实际上是一个值拷贝的过程,实际上是先将数据JSON.stringify之后再JSON.parse。主线程与子线程之间也可以交换二进制数据,比如File、Blob、ArrayBuffer等对象,也可以在线程之间发送。但是,用拷贝方式发送二进制数据,会造成性能问题。比如,主线程向子线程发送一个50MB文件,默认情况下浏览器会生成一个原文件的拷贝。为了解决这个问题,JavaScript允许主线程把二进制数据直接转移给子线程,转移后主线程无法再使用这些数据,这是为了防止出现多个线程同时修改数据的问题,这种转移数据的方法,叫做Transferable Objects。
不过现在很多浏览器支持transferable objects(可转让对象) ,这个技术是零拷贝转移,能大大提升性能,
可以指定传送的数据全都是零拷贝

var abBuffer = new ArrayBuffer(32);
aDedicatedWorker.postMessage(abBuffer, [abBuffer]);

也可以 指定某个是 使用 零拷贝

var objData = {
   "employeeId": 103,
   "name": "Sam Smith",
   "dateHired": new Date(2006, 11, 15),
   "abBuffer": new ArrayBuffer(32)
};
aDedicatedWorker.postMessage(objData, [objData.abBuffer]);

工作线程生命周期

工作线程之间的通信必须依赖于浏览器的上下文环境,并且通过它们的 MessagePort 对象实例传递消息。每个工作线程的全局作用域都拥有这些线程的端口列表,这些列表包括了所有线程使用到的 MessagePort 对象。在专用线程的情况下,这个列表还会包含隐式的 MessagePort 对象。
每个工作线程的全局作用域对象 WorkerGlobalScope 还会有一个工作线程的线程列表,在初始化时这个列表为空。当工作线程被创建的时候或者拥有父工作线程的时候,它们就会被填充进来。
最后,每个工作线程的全局作用域对象 WorkerGlobalScope 还拥有这个线程的文档模型,在初始化时这个列表为空。当工作线程被创建的时候,文档对象就会被填充进来。无论何时当一个文档对象被丢弃的时候,它就要从这个文档对象列举里面删除出来。

性能测试

初始化测试
// 部分机器webwoker初始化时间
Macbook Pro: 2 workers, 0.4 milliseconds on average
Macbook Pro: 4 workers, 0.6 milliseconds on average
Nexus 5: 2 workers, 6 milliseconds on average
Nexus 5: 4 workers, 15 milliseconds on average (border-line UI jank)
传输速度测试

1、普通json/object

2、tranferable objects


可见 transferable objects传输速度要高很多

部分典型的应用场景如下

1) 使用专用线程进行数学运算
Web Worker最简单的应用就是用来做后台计算,而这种计算并不会中断前台用户的操作
2) 图像处理
通过使用从<canvas>或者<video>元素中获取的数据,可以把图像分割成几个不同的区域并且把它们推送给并行的不同Workers来做计算
3) 大量数据的检索
当需要在调用 ajax后处理大量的数据,如果处理这些数据所需的时间长短非常重要,可以在Web Worker中来做这些,避免冻结UI线程
4) 背景数据分析
由于在使用Web Worker的时候,我们有更多潜在的CPU可用时间,我们现在可以考虑一下JavaScript中的新应用场景。例如,我们可以想像在不影响UI体验的情况下实时处理用户输入。利用这样一种可能,我们可以想像一个像Word(Office Web Apps 套装)一样的应用:当用户打字时后台在词典中进行查找,帮助用户自动纠错等等。

限制

1、不能访问DOM和BOM对象的,Location和navigator的只读访问,并且navigator封装成了WorkerNavigator对象,更改部分属性。无法读取本地文件系统
2、子线程和父级线程的通讯是通过值拷贝,子线程对通信内容的修改,不会影响到主线程。在通讯过程中值过大也会影响到性能(解决这个问题可以用transferable objects
3、并非真的多线程,多线程是因为浏览器的功能
4、兼容性
5 因为线程是通过importScripts引入外部的js,并且直接执行,其实是不安全的,很容易被外部注入一些恶意代码
6、条数限制,大多浏览器能创建webworker线程的条数是有限制的,虽然可以手动去拓展,但是如果不设置的话,基本上都在20条以内,每条线程大概5M左右,需要手动关掉一些不用的线程才能够创建新的线程(相关解决方案
7、js存在真的线程的东西,比如SharedArrayBuffer

js的多线程库

1、tagg2

参考文献:[1] www.html5rocks.com/zh...
[2] www.alloyteam.com/2015...
[3] typedarray.org/concur...
[4] www.andygup.net/advanc...
[5] developer.mozilla.org...
[6] coolaj86.github.io/htm...
[7] www.xyhtml5.com/webwor...