https://www.html5rocks.com/en/tutorials/workers/basics/
JS的并发问题
这里有一系列的瓶颈阻止JS客户端程序变得更好,如浏览器兼容性,可访问性和性能。幸运的是这些都已经成为过去,浏览器开发商提升了JS引擎的速度。
但仍然有一个障碍,那就是JS语言本身是单线程的,意味着多个脚本不能同时运行。例如,很常见是一个站点需要处理UI事件,查询和处理大量的来自服务器的数据,还要操作dom。不幸的是,浏览器由于JS运行环境的限制 ,没办法同时执行这些操作。脚本只能运行在单线程中。
开发者使用事件机制API(setTimeout、xhr、setInterval等)来模拟“并发”,这些API是异步的。好消息是相比这些hack,H5给我们提供了更好的东西。
Web worker:给JS引入线程
Web worker定义了一些API来后台运行脚本。允许我们执行一些长期运行的任务,却不会阻塞UI或者其他用于与用户进行交互的脚本。
多个worker使用类似于线程间的消息通信来实现并行
Web worker的类型
说明书(spec)中指出有两种类型:Dedicated Worker 与 Shared Worker。后续仅仅介绍 Dedicated Worker(后续简称为 DW)。
开始
DW运行在一个隔离的线程,运行的代码一般在一个独立的文件中,以下创建一个DW
var worker = new Worker('task.js');
如果指定的文件存在,则浏览器会创建一个DW线程,文件下载是异步的,如果文件返回404,则执行失败但不会报错(fail sliently)
以上创建完了一个DW之后,浏览器会异步下载脚本,下载完成后马上执行里面的代码(运行在线程中了),然后等待外部的postMessage(参数可以为空),worker内再执行内部回调函数中的代码:
worker.postMessage(); // Start the worker.
与worker通信
父页面通过worker进行通信使用postMessage传递消息。参数可支持字符串或者JSON对象。以下例子往worker中传递一个hello world,而worker中把数据回传回来:
// main var worker = new Worker('doWork.js'); worker.addEventListener('message', function(e) { console.log('Worker said: ', e.data); }, false); worker.postMessage('Hello World'); // Send data to our worker. //doWorker.js self.addEventListener('message', function(e) { self.postMessage(e.data); }, false);
通过worker.addEventListener('message',xx)或者worker.onmessage=xx来接收对方传来的消息,数据从evt.data中获取。
传递的数据都是值传递,而不是共享。如以下例子中,传递的JS对象实际上时经过序列化传递的,另一端再进行反序列化,也就是说每次传递都会在另一端重新创建一个一模一样的JS对象。
<button onclick="sayHI()">Say HI</button> <button onclick="unknownCmd()">Send unknown command</button> <button onclick="stop()">Stop worker</button> <output id="result"></output> <script> function sayHI() { worker.postMessage({'cmd': 'start', 'msg': 'Hi'}); } function stop() { // worker.terminate() from this script would also stop the worker. worker.postMessage({'cmd': 'stop', 'msg': 'Bye'}); } function unknownCmd() { worker.postMessage({'cmd': 'foobard', 'msg': '???'}); } var worker = new Worker('doWork2.js'); worker.addEventListener('message', function(e) { document.getElementById('result').textContent = e.data; }, false); </script>
doWorker2.js
self.addEventListener('message', function(e) { var data = e.data; switch (data.cmd) { case 'start': self.postMessage('WORKER STARTED: ' + data.msg); break; case 'stop': self.postMessage('WORKER STOPPED: ' + data.msg + '. (buttons will no longer work)'); self.close(); // Terminates the worker. break; default: self.postMessage('Unknown command: ' + data.msg); }; }, false);
停用worker有两种方式:worker内部调用close或者main中调用terminate方法。worker被关闭后,main中继续给他传递消息,不报错也不反应。
关于结构化克隆算法(The structured clone algorithm)
这是h5中的算法,用于复制复杂的JS对象。当通过postMessage函数与worker进行通信或者使用IndexedDB来保存对象时,内部就会用到这个算法。
克隆的方式:递归地处理输入对象,同时维持一个关于之前访问的对象的map,防止进入死循环。
性能
chrome13 和FF5 支持使用ArrayBuffer(或者Typed Array)与Worker进行通信。如(给我的感觉就是普通传递了一个数组)
// worker self.onmessage = function(e) { var uInt8Array = e.data; postMessage("Inside worker.js: uInt8Array.toString() = " + uInt8Array.toString()); postMessage("Inside worker.js: uInt8Array.byteLength = " + uInt8Array.byteLength); }; //main var uInt8Array = new Uint8Array(new ArrayBuffer(10)); for (var i = 0; i < uInt8Array.length; ++i) { uInt8Array[i] = i * 2; // [0, 2, 4, 6, 8,...] } console.log('uInt8Array.toString() = ' + uInt8Array.toString()); console.log('uInt8Array.byteLength = ' + uInt8Array.byteLength); worker.postMessage(uInt8Array);
浏览器不使用序列化和反序列化,而是使用结构化克隆算法来复制ArrayBuffer,这使父页面与worker可以使用二进制数据来进行通信。
最开始传递的数据是通过序列化和反序列化成一个JSON,这要求数据符合JSON格式,所以像File、Blo和ArrayBuffer这些类型就没办法序列化了。后来’浏览器实现了结构化克隆,允许我们传递复杂的数据(非JSON键值对对象),如File、Blob、ArrayBuffer和JSON对象,但传递这些类型数据时,依然会重新创建一个对象,这是很消耗性能的(传递一个大于32M的ArrayBuffer耗时超过几百毫秒),为了解决这个问题,需要我们使用transferable Objects(后续简称为TO)。
TO对象是一个拥有了transferable标志(tag)的对象,ArrayBuffer、MessagePort和ImageBitmap都属于是TO类型。postMessage方法可用于传递TO对象。
新版本的浏览器使用postMessage传递TO时,有很大的性能提升:数据从一个上下文传递到另一个,不会重新创建对象。但是数据传递完成后,源上下文中的对应数据的引用会被清除。
通过postMessage传递TO对象,需要使用新的postMessage签名:第一个参数是数据(可以使JSON对象也可以是ArrayBuffer),第二个参数要求必须是ArrayBuffer数组(传输的item的列表),如:
worker.postMessage({data: int8View, moreData: anotherBuffer},
[int8View.buffer, anotherBuffer]);
检测浏览器是否支持transferable
var ab = new ArrayBuffer(1); worker.postMessage(ab, [ab]); if (ab.byteLength) { alert('Transferables are not supported in your browser!'); } else { // Transferables are supported. }
worker的运行环境
可以使用self和this来应用当前worker的全局作用域。也就是说最开始的worker2.js可以这样写了:
addEventListener('message', function(e) { var data = e.data; switch (data.cmd) { case 'start': postMessage('WORKER STARTED: ' + data.msg); break; case 'stop': ... }, false); onmessage = function(e) { var data = e.data; ... };
worker内可以使用的JS特性
因为多线程的原因,可以访问的JS特性有:navigator、location(只读)、xhr、timer API、appCache,通过importScript来引入外部脚本、生成其他worker。worker不能访问的有:dom(线程不安全),window对象,document对象,parent对象。
worker内引入外部脚本
内部直接使用importScripts方法,参数可以接收多个外部脚本。
worker内创建子worker
随着多核CPU的流行,获取更好运行性能的方法是把一个大任务分解成多个worker任务。子worker文件必须与父页面同源,文件路径相对于父worker。例子:
主worker中创建子worker,给每个子worker指定一个start和end,每个子worker完成任务后主worker通过storeResult把结果传递到父页面去。
// settings var num_workers = 10; var items_per_worker = 1000000; // start the workers var result = 0; var pending_workers = num_workers; for (var i = 0; i < num_workers; i += 1) { var worker = new Worker('core.js'); worker.postMessage(i * items_per_worker); worker.postMessage((i+1) * items_per_worker); worker.onmessage = storeResult; } // handle the results function storeResult(event) { result += 1*event.data; pending_workers -= 1; if (pending_workers <= 0) postMessage(result); // finished! }
子worker根据start和end开始工作,然后把结果传递回去,最后关闭自己
var start; onmessage = getStart; function getStart(event) { start = 1*event.data; onmessage = getEnd; } var end; function getEnd(event) { end = 1*event.data; onmessage = null; work(); } function work() { var result = 0; for (var i = start; i < end; i += 1) { // perform some complex calculation here result += 1; } postMessage(result); close(); }
内联的worker
以上创建的worker都是基于一个独立的文件。其他也可以内联,要使用Blob:
var blob = new Blob(["onmessage = function(e) { postMessage('msg from worker'); }"]); // Obtain a blob URL reference to our worker 'file'. var blobURL = window.URL.createObjectURL(blob); var worker = new Worker(blobURL); worker.onmessage = function(e) { // e.data == 'msg from worker' }; worker.postMessage(); // Start the worker.
以上worker的代码通过一个字符串数组传递给Blob,createObjectURL可以创建一个url来引用Blob或者File,返回的url大致如下(疑问为什么这里不会出现跨域错误?但worker内部如果通过importScript引用了相对路径的外部脚本,则提示跨域错误):
blob:http://localhost/c745ef73-ece9-46da-8f66-ebes574789b1
这种url都是唯一的,并且一直有效直到当前页面的文档被卸载。也可以手动释放这个url,使其马上变得无效,调用:
window.URL.revokeObjectURL(blobURL);
在chrome中,访问chrome://blob-internals/
.可以看到所有被创建的blob url。
以上使用Blob来实现内联worker,但是代码写在字符串中,感觉不方便。最好是转换一下思路,那就是使用script标签来获取字符串:
<script id="worker1" type="javascript/worker"> // This script won't be parsed by JS engines // because its type is javascript/worker. self.onmessage = function(e) { self.postMessage('msg from worker'); }; // Rest of your worker code goes here. </script> <script> function log(msg) { // Use a fragment: browser will only render/reflow once. var fragment = document.createDocumentFragment(); fragment.appendChild(document.createTextNode(msg)); fragment.appendChild(document.createElement('br')); document.querySelector("#log").appendChild(fragment); } var blob = new Blob([document.querySelector('#worker1').textContent]); var worker = new Worker(window.URL.createObjectURL(blob)); worker.onmessage = function(e) { log("Received: " + e.data); } worker.postMessage(); // Start the worker. </script>
使用内联的worker时要注意,如果worker内通过importScript来引用外部脚本,要求必须是绝对路径。如果使用了相对路径则报跨域错误。解决办法就是外部传入一个当前的绝对地址,worker内部手动将相对地址拼接为绝对地址
... <script id="worker2" type="javascript/worker"> self.onmessage = function(e) { var data = e.data; if (data.url) { var url = data.url.href; var index = url.indexOf('index.html'); if (index != -1) { url = url.substring(0, index); } importScripts(url + 'engine.js'); } ... }; </script> <script> var worker = new Worker(window.URL.createObjectURL(bb.getBlob())); worker.postMessage({url: document.location}); </script>
错误处理
当一个worker正在运行时,内部出现了错误,则外部可以通过worker.addEventListener('error',xx)或者worker.onerror=xx来注册一个错误处理函数,evt中包含了3有有用的信息:
- filename:发生错误的worker脚本
- lineno:错误行号
- message:错误描述
以下是一个例子:
<output id="error" style="color: red;"></output> <output id="result"></output> <script> function onError(e) { document.getElementById('error').textContent = [ 'ERROR: Line ', e.lineno, ' in ', e.filename, ': ', e.message ].join(''); } function onMsg(e) { document.getElementById('result').textContent = e.data; } var worker = new Worker('workerWithError.js'); worker.addEventListener('message', onMsg, false); worker.addEventListener('error', onError, false); worker.postMessage(); // Start worker without a message. </script> // worker self.addEventListener('message', function(e) { postMessage(1/x); // Intentional error. };
安全
由于chrome的安全限制,worker不能本地运行(file://),否则worker会fial silently。仅当开发测试的时候,可以禁用chrome这种限制,以 --allow-file-access-from-files 标志来运行chrome。
所有worker脚本必须与当前页面同源。
应用场景
- 预获取或者数据缓存,以便于后续使用
- 代码语法高亮或者其他实时文本编辑的效果处理
- 语法检查
- 分析视频或音频数据
- 在后台对服务器的轮训或者IO
- 处理大数组或者极大的JSON数据
- canvas中的图片滤镜处理
- 大量处理web数据库