深入理解JavaScript中的并行处理
前言
为什么说多线程如此重要?这是个值得思考的问题。一直以来,派生线程以一种优雅的方式实现了对同一个进程中任务的划分。操作系统负责分配每个线程的时间片,具有高优先级并且任务繁重的线程将分配到更多的时间片,而低优先级空闲的线程只能分到较少的时间片。
虽然多线程如此重要,但JavaScript却并没有多线程的能力。幸运的是,随着WebWorker的普及,我们终于可以在后台线程来处理资源密集型的计算了。而不好的方面是,目前制定的标准只适用于当前的生态系统,这有时候就比较尴尬了。如果你了解其他从一开始就支持多线程的语言的话,你可能会发现很多的限制,远非仅仅是实例化一个新线程,然后你操控这个实例就能实现多线程。
这篇文章主要来介绍WebWorker,包括什么时候使用,该怎么使用,它有什么奇怪的特性,会介绍在Webpack中如何使用它,还有可能遇到的一些坑。
一、WebWorkers
WebWorker可能是在JavaScript中唯一可以真正实现多线程的方法了。我们需要按照下面的方式创建worker:
constworker=newWorker("worker.js");
上面就定义了一个Worker实例,然后你可以通过postMessage与worker通信,就像和iFrame通信一样,只不过不存在跨域的问题,不需要验证跨域。
worker.postMessage(num);
在worker代码中,你需要监听这些事件:
onmessage=(e)=>{ //e.datawillcontainthevaluepassed };
这种方式是双向的,所以你也可以从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代码中调用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--> <scriptid="worker"type="javascript/worker"> //Putyourworkercodehere </script> constcode=URL.createObjectURL(newBlob([ document.getElementById("worker").textContent ])); constworker=newWorker(code);
这样你就创建了一个全局的ObjectURL,但别忘了当不需要的时候要销毁它:
worker.terminate(); URL.revokeObjectURL(code);
三、Workers嵌套
理论上,你可以嵌套使用worker,就像在主线程中定义一个worker一样。这里有一个简单的例子。但是不幸的是在Chrome中一直存在一个bug,让我们不能愉快的玩耍,或许以后这个bug会修复,但是目前来说还是没有太多进展,所以你最好不要使用。
数据传递
在worker数据传递的过程中有些需要注意的边缘情况。你可以传递数值,字符串,数组,也可以传递序列化/反序列化的对象。然而,你却不应该依赖序列化来保持数据结构,实际上,postMessage用到了一种数据克隆算法,它会生成一些额外的属性比如RegExps和Blobs以及一些循环引用。
这就是说,你需要将你要传递的数据最小化。你不可以传递functions,即使是支持的类型也会有一些限制,这些也很容易产生一些难以发现的bug。如果你将你的API定义为只传递字符串,数值,数组和对象的话,那你可能会避过这些问题。
循环引用
如果你有一个很复杂的对象,那么里面很可能存在循环引用,这时如果你将它序列化成JSON,你将会得到一个TypeError:ConvertingcircularstructuretoJSON.
leta={}; letb={a}; a.b=b; JSON.stringify({a,b});//Error
然而你可以在postMessage中放心的使用,从而你就可以在worker中使用。
Transferableobjects
为了防止同时修改同一变量的场景,你传递给postMessage的所有变量都会复制一份,这样确保了你多个线程不会修改同一个变量。但如果你想要传一个非常大的数据的话,你就会发现复制操作是很慢的。比如,如果你在做一些图片相关的运算,你可能会传递整个图片信息,就可能会遇到复制性能的瓶颈。
好在有transferableobject,用transfer来代替copy,比如ArrayBuffer是transferable对象,而我们可以把任何类型的对象放在ArrayBuffer中。
如果你transfer一个对象,之前拥有它的线程被锁定权限,它确保了数据没有复制之前,不会被同时修改。
这时postMessage的代码段就有点尴尬了:
constab=newArrayBuffer(100); console.log(ab.byteLength);//100 worker.postMessage(ab,[ab]); console.log(ab.byteLength);//0
确保在postMessage中传递第二个参数,否则数据将会被复制。
constab=newArrayBuffer(100); console.log(ab.byteLength);//100 worker.postMessage(ab); console.log(ab.byteLength);//100
四、Webpack
在Webpack中使用Webworker时,你需要用worker-loader。将它添加到package.json中的devDependencies,然后运行npminstall,就可以了。
用到worker时,只需要require它。
constworkerCode=require("worker!./worker.js"); ... constworker=newworkerCode();
这样就初始化了worker,然后就像上面讲的一样使用worker。
如果需要使用行内worker,你需要传递inline参数给loader。
constworkerCode=require("worker?inline!./worker.js"); ... constworker=newworkerCode();
在worker中你也可以import模块。
importfibonaccifrom"./fibonacci.js"; ... constresult=fibonacci(num);
缺点
在Webpack中使用worker很简单,但是在使用时也有一些坑值得你注意。
首先,无法将代码共用部分提取出来。如果你的worker中依赖一段共用代码,你只能把代码添加到worker中,不管其他地方是否也用到同样的代码。而且如果你多个worker要用同样的库,你也需要在每个worker中引入它们。
你可能会想如果你不用worker-loader,然后用CommonsChunkPlugin指定一个新的入口,可能会解决这个问题。但是不幸的是worker不像是浏览器window,一些feature不可用,所以一些代码必须要引入。
同时,用行内worker也不会解决问题,共用的代码依然会出现在多个地方。
第二点缺点是,行内worker可能会导致ObjectURLs内存泄露.它们被创建出来以后就不会被释放。这虽然不是一个大问题,但是如果你有很多“一次性”worker的话,就会影响性能。
综上所述,我个人建议是使用标准的worker,注意在worker中引入了什么。还要注意使用缓存。
五、IFramesWebworker
IFramesWebworker和IFrame很像,而且印象中IFrame也可以实现多线程。但是IFrame存在一些不是线程安全API,比如DOM相关,浏览器不能为他们生成新的线程,参考这里.
在IFrame跨域中,很多API它都没有权限,也只能通过postMessage,就像WebWorker一样。理论上,浏览器可以在不同的线程中运行IFrame,也就可以用IFrame实现多线程。
但是实际并非如此,它还是单线程的,浏览器不会给它们额外的线程。
总结
WebWorker解决了JavaScript一直以来的大难题,尽管它的语法有些奇怪而且有很多限制,但是它却可以真真正正的解决问题。从另外一方面来讲,它也还是个婴儿,某些方面还不是很成熟,不能让我们完全依赖,所以这个技术普及还有一段距离,目前适用场景也比较局限。所以说,如果你需要做多线程,不要再等待其他的什么技术,学习webworker的边缘问题,避开它的坑,你就可以很好的提高用户体验。以上就是这篇文章的全部内容,希望对大家能有所帮助。