深入理解JavaScript中的并行处理
前言
为什么说多线程如此重要?这是个值得思考的问题。一直以来,派生线程以一种优雅的方式实现了对同一个进程中任务的划分。操作系统负责分配每个线程的时间片,具有高优先级并且任务繁重的线程将分配到更多的时间片,而低优先级空闲的线程只能分到较少的时间片。
虽然多线程如此重要,但JavaScript却并没有多线程的能力。幸运的是,随着 Web Worker 的普及,我们终于可以在后台线程来处理资源密集型的计算了。而不好的方面是,目前制定的标准只适用于当前的生态系统,这有时候就比较尴尬了。如果你了解其他从一开始就支持多线程的语言的话,你可能会发现很多的限制,远非仅仅是实例化一个新线程,然后你操控这个实例就能实现多线程。
这篇文章主要来介绍 Web Worker,包括什么时候使用,该怎么使用,它有什么奇怪的特性,会介绍在 Webpack 中如何使用它,还有可能遇到的一些坑。
一、Web Workers
Web Worker 可能是在 JavaScript 中唯一可以真正实现多线程的方法了。我们需要按照狼蚁网站SEO优化的方式创建 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) => {};
为了调试方便,异常对象中还有一些额外的字段比如filename
,lineno
,colno
.
回收
将不需要的 worker 回收是非常重要的,worker 会生成真正的操作系统线程,如果你发现与很多 worker 线程运行,你可以通过很简单的杀掉浏览器进程。
你有两种方式杀掉 worker 进程在 worker 里和在 worker 外。我认为最好的处理 worker 生命周期的地方是在主页面里,但这也要取决于你代码的具体情况。
杀掉一个 worker 实例,在外部可以直接调用 terminate()
方法,这种方法可以立即杀掉它,释放所有它正在使用的资源,如果它正在运行,也会立即终止。
如果你想要让 worker 自己去管理它的生命周期,可以直接在 worker 代码中调用s()
方法。
不管使用哪种方法,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./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 fibonai from "./fibonai.js"; ... const result = fibonai(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 的边缘问题,避开它的坑,你就可以很好的提高用户体验。以上就是这篇文章的全部内容,希望对大家能有所帮助。
编程语言
- 如何快速学会编程 如何快速学会ug编程
- 免费学编程的app 推荐12个免费学编程的好网站
- 电脑怎么编程:电脑怎么编程网咯游戏菜单图标
- 如何写代码新手教学 如何写代码新手教学手机
- 基础编程入门教程视频 基础编程入门教程视频华
- 编程演示:编程演示浦丰投针过程
- 乐高编程加盟 乐高积木编程加盟
- 跟我学plc编程 plc编程自学入门视频教程
- ug编程成航林总 ug编程实战视频
- 孩子学编程的好处和坏处
- 初学者学编程该从哪里开始 新手学编程从哪里入
- 慢走丝编程 慢走丝编程难学吗
- 国内十强少儿编程机构 中国少儿编程机构十强有
- 成人计算机速成培训班 成人计算机速成培训班办
- 孩子学编程网上课程哪家好 儿童学编程比较好的
- 代码编程教学入门软件 代码编程教程