深入理解JavaScript中的并行处理

前言

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

虽然多线程如此重要,但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 的边缘问题,避开它的坑,你就可以很好的提高用户体验。以上就是这篇文章的全部内容,希望对大家能有所帮助。

(0)

相关推荐

  • python开启多个子进程并行运行的方法

    本文实例讲述了python开启多个子进程并行运行的方法.分享给大家供大家参考.具体如下: 这个python代码创建了多个process子进程,创建完成后先start(),最后统一join,这样所有子进程会并行执行. from multiprocessing import Process import sys, os import time def timetask(times): time.sleep(times) print time.localtime() def works(func, a

  • JQuery处理json与ajax返回JSON实例代码

    一.JSON的一些基础知识. JSON中对象通过"{}"来标识,一个"{}"代表一个对象,如{"AreaId":"123"},对象的值是键值对的形式(key:value). "[]",标识数组,数组内部各个数据之间通过","分割,如["AreaId":"123","AreaId":"345"]. 很多情况下是

  • Linux BASH多进程并行处理的方法实现

    复制代码 代码如下: #!/bin/bash SEND_THREAD_NUM=13 tmp_fifofile="/tmp/$$.fifo" # 脚本运行的当前进程ID号作为文件名 mkfifo "$tmp_fifofile" # 新建一个随机fifo管道文件 exec 6<>"$tmp_fifofile" # 定义文件描述符6指向这个fifo管道文件 rm $tmp_fifofile for ((i=0;i<$SEND_THRE

  • jQuery Ajax异步处理Json数据详解

    先我们来看一个官方的实例使用 AJAX 请求来获得 JSON 数据,并输出结果: 复制代码 代码如下: $("button").click(function(){  $.getJSON("demo_ajax_json.js",function(result){    $.each(result, function(i, field){      $("div").append(field + " ");    });  });

  • js中同步与异步处理的方法和区别总结

    在使用异步请求时,有时需要将异步请求的结果返回给另一个js函数,此种情况下会出现未等异步请求返回请求结果,该发送请求所在js函数已经执行完后续操作,即已经执行return ,这样会导致return的结果为空字符. 总结:若要在使用ajax请求后处理发送请求返回的结果,最好使用同步请求. 例如:以下例子会出现返回结果不正确的情况,因为ajax异步请求还未执行完,函数已经执行return了, 复制代码 代码如下: function fn(){ var result = " "; $.aja

  • 用javascript实现截取字符串包含中文处理的函数

    1.substring 方法 定义和用法 substring 方法用于提取字符串中介于两个指定下标之间的字符. 语法 stringObject.substring(start,stop) 参数 描述 start 必需.一个非负的整数,规定要提取的子串的第一个字符在 stringObject 中的位置. stop 可选.一个非负的整数,比要提取的子串的最后一个字符在 stringObject 中的位置多 1.如果省略该参数,那么返回的子串会一直到字符串的结尾. 返回值 一个新的字符串,该字符串值包

  • js图片自动切换效果处理代码

    复制代码 代码如下: <script language =javascript > var curIndex=0; //时间间隔 单位毫秒 var timeInterval=1000; var arr=new Array(); arr[0]="1.jpg"; arr[1]="2.jpg"; arr[2]="3.jpg"; arr[3]="4.jpg"; arr[4]="5.jpg"; arr[5

  • javascript 容错处理代码(屏蔽js错误)

    国外银行页面就是因为这样的错误,导致用户大量流失不敢用这个银行的网上银行等导致损失惨重. 所以一个想要在用户体验与公司实力上都是不容出现的错误,就是不影响使用也不能让用户反感,我们一直致力于为大家提高更好的代码,所以这里我们将会大家整理一些常用的代码,使用方法什么,希望大家以后能越来越支持我们,让我们共同发展. 第一种:我们自己也在用的 复制代码 代码如下: <SCRIPT language=javascript> <!-- window.onerror=function(){retur

  • jquery序列化form表单使用ajax提交后处理返回的json数据

    1.返回json字符串: 复制代码 代码如下: /** 将一个字符串输出到浏览器 */     protected void writeJson(String json) {         PrintWriter pw = null;         try {             servletResponse.setContentType("text/plain;charset=UTF-8");             pw = servletResponse.getWrit

  • 用Oracle并行查询发挥多CPU的威力

    正在看的ORACLE教程是:用Oracle并行查询发挥多CPU的威力.参数 让我们进一步看看CPU的数量是如何影响这些参数的. 参数fast_start_parallel_rollback Oracle并行机制中一个令人兴奋之处是在系统崩溃时调用并行回滚得能力.当Oracle数据库发生少有的崩溃时,Oracle能自动检测未完成的事务并回滚到起始状态.这被称为并行热启动,而Oracle使用基于cpu_count的fast_start_parallel_rollback参数来决定未完成事务的秉性程度

随机推荐