深入理解JavaScript中的并行处理


Posted in Javascript onSeptember 22, 2016

前言

为什么说多线程如此重要?这是个值得思考的问题。一直以来,派生线程以一种优雅的方式实现了对同一个进程中任务的划分。操作系统负责分配每个线程的时间片,具有高优先级并且任务繁重的线程将分配到更多的时间片,而低优先级空闲的线程只能分到较少的时间片。

虽然多线程如此重要,但JavaScript却并没有多线程的能力。幸运的是,随着 Web Worker 的普及,我们终于可以在后台线程来处理资源密集型的计算了。而不好的方面是,目前制定的标准只适用于当前的生态系统,这有时候就比较尴尬了。如果你了解其他从一开始就支持多线程的语言的话,你可能会发现很多的限制,远非仅仅是实例化一个新线程,然后你操控这个实例就能实现多线程。

这篇文章主要来介绍 Web Worker,包括什么时候使用,该怎么使用,它有什么奇怪的特性,会介绍在 Webpack 中如何使用它,还有可能遇到的一些坑。

一、Web Workers

Web Worker 可能是在 JavaScript 中唯一可以真正实现多线程的方法了。我们需要按照下面的方式创建 worker :

const worker = newWorker("worker.js");

上面就定义了一个 Worker 实例,然后你可以通过 postMessage 与 worker 通信,就像和 iFrame 通信一样,只不过不存在跨域的问题,不需要验证跨域。

worker.postMessage(num);

在 worker 代码中,你需要监听这些事件:

onmessage = (e) => {
 // e.data will contain the value passed
};

这种方式是双向的,所以你也可以从 worker 中 postMessage 给我们的主程序。

在 worker 代码中:

postMessage(result);

在主程序中:

worker.onmessage = (e) => {}

这就是 worker 最基本的用法。

异常处理

在你的 worker 代码中,有很多种方式来处理异常,比如你可以 catch 之后通过 postMessage 传递,这样可能需要多些一些代码,但是确实最有效也最安全的。

另一种方式是用 onerror 事件,这种方式可以捕捉所有未处理的异常,并且交给调用方来决定如何处理。调用方式很简单:

worker.onerror = (e) => {};

为了调试方便,异常对象中还有一些额外的字段比如:filenamelinenocolno.

回收

将不需要的 worker 回收是非常重要的,worker 会生成真正的操作系统线程,如果你发现与很多 worker 线程同时运行,你可以通过很简单的杀掉浏览器进程。

你有两种方式杀掉 worker 进程:在 worker 里和在 worker 外。我认为最好的处理 worker 生命周期的地方是在主页面里,但这也要取决于你代码的具体情况。

杀掉一个 worker 实例,在外部可以直接调用 terminate()方法,这种方法可以立即杀掉它,释放所有它正在使用的资源,如果它正在运行,也会立即终止。

如果你想要让 worker 自己去管理它的生命周期,可以直接在 worker 代码中调用stop()方法。

不管使用哪种方法,worker 都会停止,销毁所有资源。

如果你想使用一种“一次性”的 worker,比如需要做一些复杂运算之后就不再使用了,也要确保在 onerror 事件中去销毁它们,这样有利于规避一些难以发现的问题。

worker.onerror = (e) => {
 worker.terminate();
 reject(e);
};
worker.onmessage = (e) => {
 worker.terminate();
 resolve(e.data);
}

二、行内 Workers

有些时候将 worker 代码写到一个外部文件可能会使原本简单的问题变得复杂,幸运的是,workers 也可以用一个 Blob 来初始化。

写一个行内 worker ,参考如下代码段:

<!-- http://stackoverflow.com/a/6454685/2032154 -->
<script id="worker" type="javascript/worker">
 // Put your worker code here
</script>
const code = URL.createObjectURL(new Blob([
 document.getElementById("worker").textContent
]));
const worker = new Worker(code);

这样你就创建了一个全局的 ObjectURL,但别忘了当不需要的时候要销毁它:

worker.terminate();
URL.revokeObjectURL(code);

三、Workers 嵌套

理论上,你可以嵌套使用 worker,就像在主线程中定义一个 worker 一样。这里有一个简单的 例子。但是不幸的是在 Chrome 中一直存在一个 bug ,让我们不能愉快的玩耍,或许以后这个 bug 会修复,但是目前来说还是没有太多进展,所以你最好不要使用。

数据传递

在 worker 数据传递的过程中有些需要注意的边缘情况。你可以传递数值,字符串,数组,也可以传递序列化/反序列化的对象。然而,你却不应该依赖序列化来保持数据结构,实际上,postMessage 用到了一种 数据克隆算法,它会生成一些额外的属性比如 RegExps 和 Blobs 以及一些循环引用。

这就是说,你需要将你要传递的数据最小化。你不可以传递 functions ,即使是支持的类型也会有一些限制,这些也很容易产生一些难以发现的 bug。如果你将你的 API 定义为只传递字符串,数值,数组和对象的话,那你可能会避过这些问题。

循环引用

如果你有一个很复杂的对象,那么里面很可能存在循环引用,这时如果你将它序列化成 JSON,你将会得到一个 TypeError: Converting circular structure to JSON.

let a = {};
let b = {a};
a.b = b;
JSON.stringify({a,b}); // Error

然而你可以在 postMessage 中放心的使用,从而你就可以在 worker 中使用。

Transferable objects

为了防止同时修改同一变量的场景,你传递给 postMessage 的所有变量都会复制一份,这样确保了你多个线程不会修改同一个变量。但如果你想要传一个非常大的数据的话,你就会发现复制操作是很慢的。比如,如果你在做一些图片相关的运算,你可能会传递整个图片信息,就可能会遇到复制性能的瓶颈。

好在有 transferable object ,用 transfer 来代替 copy,比如ArrayBuffer 是transferable对象,而我们可以把任何类型的对象放在 ArrayBuffer 中。

如果你 transfer 一个对象,之前拥有它的线程被锁定权限,它确保了数据没有复制之前,不会被同时修改。

这时 postMessage 的代码段就有点尴尬了:

const ab = new ArrayBuffer(100);
console.log(ab.byteLength); // 100
worker.postMessage(ab, [ab]);
console.log(ab.byteLength); // 0

确保在 postMessage 中传递第二个参数,否则数据将会被复制。

const ab = new ArrayBuffer(100);
console.log(ab.byteLength); // 100
worker.postMessage(ab);
console.log(ab.byteLength); // 100

四、Webpack

在 Webpack 中使用 Web worker 时,你需要用 worker-loader。将它添加到 package.json 中的 devDependencies,然后运行 npm install,就可以了。

用到 worker 时,只需要 require 它。

const workerCode = require("worker!./worker.js");
...
const worker = new workerCode();

这样就初始化了 worker,然后就像上面讲的一样使用 worker。

如果需要使用行内 worker,你需要传递 inline 参数给 loader。

const workerCode = require("worker?inline!./worker.js");
...
const worker = new workerCode();

在 worker 中你也可以 import 模块。

import fibonacci from "./fibonacci.js";
...
const result = fibonacci(num);

缺点

在 Webpack 中使用 worker 很简单,但是在使用时也有一些坑值得你注意。

首先,无法将代码共用部分提取出来。如果你的 worker 中依赖一段共用代码,你只能把代码添加到 worker 中,不管其他地方是否也用到同样的代码。而且如果你多个 worker 要用同样的库,你也需要在每个 worker 中引入它们。

你可能会想如果你不用 worker-loader,然后用CommonsChunkPlugin指定一个新的入口,可能会解决这个问题。但是不幸的是 worker 不像是浏览器 window ,一些 feature 不可用,所以一些代码必须要引入。

同时,用行内 worker 也不会解决问题,共用的代码依然会出现在多个地方。

第二点缺点是,行内 worker 可能会导致 ObjectURLs内存泄露.它们被创建出来以后就不会被释放。这虽然不是一个大问题,但是如果你有很多“一次性” worker 的话,就会影响性能。

综上所述,我个人建议是使用标准的 worker,注意在 worker 中引入了什么。还要注意使用缓存。

五、IFrames Web worker

IFrames Web worker 和 IFrame 很像,而且印象中 IFrame 也可以实现多线程。但是 IFrame 存在一些不是线程安全 API,比如 DOM 相关,浏览器不能为他们生成新的线程,参考这里.

在 IFrame 跨域中,很多 API 它都没有权限,也只能通过 postMessage,就像 Web Worker 一样。理论上,浏览器可以在不同的线程中运行 IFrame,也就可以用 IFrame 实现多线程。

但是实际并非如此,它还是单线程的,浏览器不会给它们额外的线程。

总结

Web Worker 解决了 JavaScript 一直以来的大难题,尽管它的语法有些奇怪而且有很多限制,但是它却可以真真正正的解决问题。从另外一方面来讲,它也还是个婴儿,某些方面还不是很成熟,不能让我们完全依赖,所以这个技术普及还有一段距离,目前适用场景也比较局限。所以说,如果你需要做多线程,不要再等待其他的什么技术,学习 web worker 的边缘问题,避开它的坑,你就可以很好的提高用户体验。以上就是这篇文章的全部内容,希望对大家能有所帮助。

Javascript 相关文章推荐
打造个性化的功能强大的Jquery虚拟键盘(VirtualKeyboard)
Oct 11 Javascript
javascript实现rgb颜色转换成16进制格式
Jul 10 Javascript
JavaScript快速切换繁体中文和简体中文的方法及网站支持简繁体切换的绝招
Mar 07 Javascript
解决ztree搜索中多级菜单展示不全问题
Jul 05 Javascript
vue使用xe-utils函数库的具体方法
Mar 06 Javascript
微信小程序如何像vue一样在动态绑定类名
Apr 17 Javascript
vue实现密码显示与隐藏按钮的自定义组件功能
Apr 23 Javascript
如何基于js判断浏览器版本
Feb 20 Javascript
Vue3新特性之在Composition API中使用CSS Modules
Jul 13 Javascript
JQuery基于FormData异步提交数据文件
Sep 01 jQuery
解决Vue watch里调用方法的坑
Nov 07 Javascript
深入理解Vue的数据响应式
May 15 Vue.js
Actionscript与javascript交互实例程序(修改)
Sep 22 #Javascript
Javascript 调用 ActionScript 的简单方法
Sep 22 #Javascript
JavaScript与ActionScript3两者的同性与差异性
Sep 22 #Javascript
ionic由于使用了header和subheader导致被遮挡的问题的两种解决方法
Sep 22 #Javascript
自制微信公众号一键排版工具
Sep 22 #Javascript
IONIC自定义subheader的最佳解决方案
Sep 22 #Javascript
详解Node.js中的事件机制
Sep 22 #Javascript
You might like
PHP+DBM的同学录程序(1)
2006/10/09 PHP
PHP随机数生成代码与使用实例分析
2011/04/08 PHP
PHP新手NOTICE错误常见解决方法
2011/12/07 PHP
php实现图片缩略图的方法
2016/03/29 PHP
PHP反射API示例分享
2016/10/08 PHP
jQueryUI如何自定义组件实现代码
2010/11/14 Javascript
jquery中获得元素尺寸和坐标的方法整理
2014/05/18 Javascript
D3.js中data(), enter() 和 exit()的问题详解
2015/08/17 Javascript
jquery获取点击控件的绝对位置简单实例
2016/10/13 Javascript
JS基于递归实现倒计时效果的方法
2016/11/26 Javascript
关于jQuery EasyUI 中刷新Tab选项卡后一个页面变形的解决方法
2017/03/02 Javascript
JavaScript如何获取到导航条中HTTP信息
2017/10/10 Javascript
webpack中使用iconfont字体图标的方法
2018/02/22 Javascript
15个顶级开源JavaScript框架和库
2018/10/10 Javascript
vue单页面在微信下只能分享落地页的解决方案
2019/04/15 Javascript
layui实现多图片上传并限制上传的图片数量
2019/09/26 Javascript
vue父子组件间引用之$parent、$children
2020/05/20 Javascript
vue输入框使用模糊搜索功能的实现代码
2020/05/26 Javascript
[48:56]2018DOTA2亚洲邀请赛 3.31 小组赛 A组 VG vs KG
2018/03/31 DOTA
python实现多线程采集的2个代码例子
2014/07/07 Python
DJANGO-ALLAUTH社交用户系统的安装配置
2014/11/18 Python
python实现画五角星和螺旋线的示例
2019/01/20 Python
Python基于scipy实现信号滤波功能
2019/05/08 Python
基于Keras 循环训练模型跑数据时内存泄漏的解决方式
2020/06/11 Python
HTML5本地存储之IndexedDB
2017/06/16 HTML / CSS
Adobe Html5 Extension开发初体验图文教程
2017/11/14 HTML / CSS
C,C++的几个面试题小集
2013/07/13 面试题
数据库的约束含义
2012/09/09 面试题
shallow copy和deep copy的区别
2016/05/09 面试题
2014年质检工作总结
2014/11/26 职场文书
2014年图书管理员工作总结
2014/12/01 职场文书
认真学习保证书
2015/02/26 职场文书
大专护理专业自荐信
2015/03/25 职场文书
nginx简单配置多个server的方法
2021/03/31 Servers
JS监听Esc 键触发事键
2021/04/14 Javascript
理解python中装饰器的作用
2021/07/21 Python