[书籍翻译] 《JavaScript并发编程》第五章 使用Web Workers

本文是我翻译《JavaScript Concurrency》书籍的第五章 使用Web Workers,该书主要以Promises、Generator、Web workers等技术来说解JavaScript并发编程方面的实践。javascript

完整书籍翻译地址:https://github.com/yzsunlei/javascript_concurrency_translation 。因为能力有限,确定存在翻译不清楚甚至翻译错误的地方,欢迎朋友们提issue指出,感谢。html

Web workers在Web浏览器中实现了真正的并发。它们花了不少时间改进,如今已经有了很好的浏览器支持。在Web workers以前,咱们的JavaScript代码局限于CPU,咱们的执行环境在页面首次加载时启动。Web workers发展起来后 - Web应用程序愈来愈强大。他们也开始须要更多的计算能力。与此同时,多核CPU如今很常见 - 即便是在一些低端设备上。java

在本章中,咱们将介绍Web workers的思想,以及它们如何与咱们努力在应用中实现的并发性原则产生关联。而后,将经过示例学习如何使用Web worker,以便在本书的后面部分,咱们能够开始将并发与咱们已经探索过的其余一些想法联系起来,例如promises和generators。node

什么是Web workers?

在深刻研究实现示例以前,本节将简要介绍Web workers的概念。搞清楚Web workers如何与引擎下的其余系统协做的。Web workers是操做系统线程 - 咱们能够调度事件的对象,它们以真正的并发范式来执行咱们的JavaScript代码。git

OS线程

从本质上讲,Web workers只不过是操做系统级线程。线程有点像进程,除了它们须要更少的开销,由于它们与建立它们的进程共享内存地址。因为为Web workers提供支持的线程处于操做系统级别,所以受系统及其进程调度程序的管理。实际上,这正是咱们想要的 - 让内核清楚咱们的JavaScript代码应该何时运行,这样才能充分地利用CPU。程序员

下面的示图展现了浏览器如何将其Web workers映射到OS线程,以及这些线程如何映射到CPU上:github

image090.gif

在平常活动结束时,操做系统最好能放下其余任务来负责它擅长的 - 处理物理硬件上的软件任务调度。在传统的多线程编程环境中,代码更接近操做系统内核。Web workers不是这种状况。虽然底层机制是一个线程,可是暴露的编程接口看起来更像是你可能在DOM中查找的东西。web

事件对象

Web workers实现了熟悉的事件对象接口。这使得Web workers的行为相似于咱们使用的其余组件,例如DOM元素或XHR请求。Web workers触发事件,这就是咱们在主线程中从他们那里接收数据的方式。咱们也能够向Web workers发送数据,这使用一个简单的方法调用。编程

当咱们将数据传递给Web workers时,咱们实际上会触发另外一个事件;只有这时候,它位于Web workers的执行上下文中,而不是在主页面的执行上下文。没有更多的事情要处理:数据输入,数据输出。没有互斥结构或任何此类结构。这其实是一件好事,由于做为平台的Web浏览器已经有许多模块。想象一下,若是咱们投入很复杂的多线程模型而不是一个简单的基于事件对象的方法。咱们天天已经有足够多的bugs须要处理。json

如下是关于Web worker排布的样子,相对于生成这些Web workers的主线程:

image091.gif

真正的并发

Web workers是在咱们的架构中实现并发原则的方法。咱们知道,Web workers是操做系统线程,这意味着在它们内部运行的JavaScript代码可能在与主线程中的某些DOM事件处理程序代码相同的实例上运行。可以作这样的事情已经在很长一段时间成为JavaScript程序员的目标了。在Web workers以前,真正的并发性是不可能的。咱们所作的最好的就是模拟它,给用户一种许多事情同时发生的的假象。

可是,始终在同一CPU内核上运行是存在问题的。咱们从根本上限制了在给定时间窗口内能够执行多少次计算。当引入真正的并发性时,此限制会被打破,由于能够运行计算的时间窗口会随着添加的CPU而增长。

话虽这么说,对于咱们的应用程序所作的大多数事情,单线程模型工做的也很好。如今的机器都很强大。咱们能够在很短的时间内完成不少工做。当咱们临近峰值时会出现问题。这些多是一些事件中断了咱们代码处理进程。咱们的应用程序不断被要求作得更多 - 更多功能,更多数据。

Web workers所关心的就是咱们能够更好地利用咱们面前的硬件的方法。Web workers,若是使用得当,它不必定是咱们在项目中永远不会使用的不可逾越的新东西,由于它的概念超出咱们以前的理解。

workers的种类

在开发并发JavaScript应用程序中,咱们可能会见到三种类型的Web workers。在本节中,咱们将比较这三种类型,以即可以了解在给定的上下文中哪一种类型的workers更有用。

专用workers

专用workers多是最多见的workers类型。它们被做为是Web worker的默认类型。当咱们的页面建立一个新的Web worker时,它专门用于页面的执行上下文而不是其余内容。当咱们的页面销毁时,页面建立的全部专用workers也会销毁。

页面与其建立的任何专用worker之间的通讯方式很是简单。该页面将消息发送给workers,workers又将消息发回页面。这些消息的顺序取决于咱们尝试使用Web worker解决的问题。咱们将在本书中深刻研究这些消息传递模式。

术语主线程和页面在本书中是同义词。主线程是典型的执行上下文,咱们能够在这里操做页面并监听输入。
Web worker上下文基本相同,但只能访问较少的Web组件。咱们将很快讨论这些限制。

如下是页面与专用workers通讯的描述:

image095.gif

正如咱们所看到的那样,专用workers是专一的。它们仅用来服务建立它们的页面。他们不直接与其余Web workers通讯,也没法与任何其余页面进行通讯。

子workers

子workers与专用workers很是类似。主要区别在于它们是由专门的Web worker建立的,而不是由主线程建立的。例如,若是专用workers的任务能够从并发执行中受益,则能够生成子workers并协调子workers之间的任务执行。

除了拥有不一样的建立者以外,子workers还具备一些与专用workers相同的特征。子workers不直接与主线程中运行的JavaScript通讯。由建立它们的worker来协调他们的通讯。如下有张示图,说明子workers如何按照约定来运行的:

image096.gif

共享workers

第三类Web worker被称为一个共享worker。共享workers被如此命名是由于多个页面能够共享这种类型worker的同一个实例。在该页面能够访问一个给定的共享workers实例由同源策略所限制,这意味着,若是一个页面跟这个worker不一样域,该worker是不被容许与此页面通讯的。

共享workers解决的问题与专用workers解决的问题不一样。将专用workers视为没有反作用的函数。你将数据传递给它们并得到不一样的返回数据。将共享workers视为遵循单例模式的应用程序对象。它们是在不一样上下文之间共享状态的方法。所以,例如,咱们不会仅仅为了处理数字而建立一个共享worker; 咱们可使用一个专用worker。

当内存中的应用程序数据来自同一应用程序的其余页面时,咱们使用共享workers就有意义了。想一想用户在新选项卡中打开连接。这将建立一个新的上下文。这也意味着咱们的JavaScript组件须要经历获取页面所需的全部数据,执行全部初始化步骤等过程。这形成重复和浪费。为何不经过在不一样的浏览上下文之间共享的方式来保存这些资源呢?如下有个示图说明来自同一应用程序的多个页面与共享workers实例通讯:

image097.gif

实际上还有第四种类型称为服务workers。这些是共享worker,其中包含与缓存网络资源和脱机功能相关的其余功能。服务workers仍处于规范的早期阶段,但他们看起来颇有意义。若是服务workers成为可行的Web技术,咱们今天了解的关于共享workers的任何内容都将适用于服务workers。

这里要考虑的另外一个重要因素是服务workers的复杂性。主线程和服务worker之间的通讯机制涉及使用端口。一样,在共享workers中运行的代码须要确保它经过正确的端口进行通讯。咱们将在本章后面更深刻地介绍共享workers的通讯。

Web workers环境

Web worker环境与咱们的代码一般运行的JavaScript环境不一样。在本节中,咱们将指出主线程的JavaScript环境与Web worker线程之间的主要区别。

什么是可用的,什么不是?

对Web workers的一个常见误解是,它们与默认的JavaScript执行上下文彻底不一样。确实,他们是不一样的,但没有那么不一样以致于没有可比性。也许,正是因为这个缘由,JavaScript开发人员在可能的时候回避使用Web worker是有好处的。

明显的差距是DOM - 它在Web worker执行环境中不存在。它不存在是规范起草者有意识决定的。经过避免DOM集成到worker线程中,浏览器提供商能够避免许多潜在的特殊状况。咱们都很是重视浏览器的稳定性,或者至少咱们应该重视。从Web worker那里获取DOM访问权限真的很方便吗?咱们将在本书接下来的几章中看到,workers擅长许多其余任务,这些任务最终有助于成功实现并发原则。

因为咱们的Web worker代码没有DOM访问权限,所以咱们不太可能自找麻烦。它实际上迫使咱们去思考为何咱们要使用Web workers。咱们实际上可能退后一步,从新思考咱们的方法。除了DOM以外,咱们平常使用的大部分功能权限都有,这正是咱们所指望的。这包括在Web workers中使用咱们喜欢的类库。

有关Web worker执行环境中缺乏功能的更详细分类,请参阅此页面
https://developer.mozilla.org/en-US/docs/Web/API/Worker/Functions_
and_classes_available_to_workers

加载脚本

咱们毫不会将整个应用程序编写在一个JavaScript文件中。相反,咱们经过将源代码划分为文件的方式来便于模块化,从逻辑上能够将设计分解为咱们想映射的内容。一样,咱们可能不但愿有由数千行代码组成的Web workers。幸运的是,Web worker提供了一种机制,容许咱们将代码导入到咱们的Web worker中。

第一种场景是将咱们本身的代码导入到一个Web worker上下文。咱们极可能有许多低级别的工具方法是专门针对咱们的应用程序。有很大可能,咱们就须要在两个环境使用这些工具:一个普通的脚本环境和一个worker线程。咱们想要保持代码的模块化,并但愿代码以相同的方式做用于Web workers环境,就像它会在任何其余环境下运行。

第二种场景是在Web workers中加载第三方库。这与将咱们本身的模块加载到Web workers中的原理相同 - 咱们的代码能够在任何上下文中使用,但有一些例外,例如DOM代码。让咱们看一个建立Web worker并加载lodash库的示例。首先,咱们将启动Web worker:

//加载Web worker脚本,
//而后启动Web worker线程。
var worker = new Worker('worker.js');

接下来,咱们将使用loadScripts()函数将lodash库导入咱们的库:

//导入lodash库,
//让全局“_”变量在Web worker上下文中可用。
importScripts('lodash.min.js');

//咱们如今能够在Web worker中使用库。
console.log('in worker', _.at([1, 2, 3], 0, 2));
//→in worker[1,3]

在开始使用脚本以前,咱们不须要担忧等待脚本加载 - importScripts()是一个阻塞的操做。

与Web workers通讯

前面的示例建立了一个Web worker,它确实在本身的线程中运行。可是,这对咱们没有多大帮助,由于咱们须要可以与咱们创造的workers通讯。在本节中,咱们将介绍从Web workers发送和接收消息所涉及的基本机制,包括如何序列化这些消息。

发布消息

当咱们想要将数据传递给Web worker时,咱们使用postMessage()方法。顾名思义,此方法将给定的消息发送给worker。若是在worker中设置了任何消息事件处理程序,它们将响应此调用。让咱们看一个将字符串发送给worker的基本示例:

//启动Web worker线程。
var worker = new Worker('worker.js');

//向Web worker发送消息,
//触发“message”事件处理程序。
worker.postMessage('hello world');

如今让咱们看看worker经过为消息对象设置事件处理程序来查看此响应消息:

//为任何“message”设置事件监听器
//调度给该worker的事件。
addEventListener('message', (e) => {

    //能够经过事件对象的“data”属性访问发送的数据
    console.log(e.type, `"${e.data}"`);
    //→message “hello world”
});

addEventListener()函数是在全局专用Web workers环境调用的。
咱们能够将其视为Web workers的窗口对象。

消息序列化

从主线程传递到worker线程的消息数据要通过序列化转换。当此序列化数据到达worker线程时,它被反序列化,而且数据可用做JavaScript基本类型。当worker线程想要将数据发送回主线程时,使用一样的过程。

毋庸置疑,这是一个多余的步骤,给咱们可能已通过度工做的应用程序增长了开销。所以,必须考虑在线程之间来回传递数据,由于从CPU成本方面来讲这不是轻松的操做。在本书的Web worker代码示例中,咱们将消息序列化视为咱们并发决策过程当中的关键因素。

因此问题是 - 为何要这么长?若是咱们在JavaScript代码中使用的worker只是线程,咱们应该在技术上可以使用相同的对象,由于这些线程使用相同的内存地址段。当线程共享资源(例如内存中的对象)时,可能会发生具备挑战性的资源抢占状况。例如,若是一个worker锁定一个对象而另外一个worker试图使用它,则这会发生错误。咱们必须实现逻辑来优雅地等待对象变得可用,而且咱们必须在worker中实现逻辑来释放锁定的资源。

简而言之,这是一个容易出错的使人头痛的问题,若是没有这个问题咱们会好得多。值得庆幸的是,在仅序列化消息的线程之间没有共享资源。这意味着咱们在实际传递给worker的东西方面受到限制。经验上是传递能够编码为JSON字符串的东西一般是安全的。请记住,worker必须今后序列化字符串重建对象,所以函数或类实例的字符串表示根本将不起做用。让咱们经过一个例子来看看它是如何工做的。首先,看一个简单的worker记录它收到的消息:

//简单输出收到的消息。
addEventListener('message', (e) => {
    console.log('message', e.data);
});

如今让咱们看看使用postMessage()能够序列化哪一种类型的数据并发送给这个worker:

//启动Web worker
var worker = new Worker('worker.js');

//发送一个普通对象。
worker.postMessage({hello: 'world'});
//→消息{hello:"world"}

//发送一个数组。
worker.postMessage([1, 2, 3]);
//→消息[1,2,3]

//试图发送一个函数,结果抛出错误
worker.postMessage(setTimeout);
//→未捕获的DataCloneError

咱们能够看到,当咱们尝试将函数传递给postMessage()时会出现一些问题。这种数据类型一旦到达worker线程就没法重建,所以,postMessage()只能抛出异常。这些类型的限制可能看起来过于局限,但它们确实消除了许多可能出现的并发问题。

接收来自workers的消息

若是没有将数据传回主线程的能力,workers对咱们来讲就没什么用了。在某些时候,workers执行的任务须要显示在UI中。咱们可能还记得,worker实例是事件对象。这意味着咱们能够监听消息事件,并在workers发回数据时作出相应的响应。能够将此视为向workers发送数据的反向。workers经过向主线程发送消息将主线程视为另外一个workers线程,而主线程则侦听消息。咱们在上一节中探讨的序列化限制在这里也是同样的。

让咱们看一下将消息发送回主线程的一些worker代码:

//2秒后,使用“postMessage()”函数将一些数据发回给主线程。
setTimeout(() => {
    postMessage('hello world');
}, 2000);

咱们能够看到,这个worker启动了,2秒后,将一个字符串发送回主线程。如今,让咱们看看如何在主JavaScript环境中处理这些传入的消息:

//启动一个worker线程。
var worker = new Worker('worker.js');

//为“message”对象添加一个事件侦听器,
//注意“data”属性包含实际的消息数据,
//与发送消息给workers的方式相同。
worker.addEventListener('message', (e) => {
    console.log('from worker', `"$ {e.data}"`);
});

您可能已经注意到咱们没有显式终止任何worker线程。这不要紧。当浏览上下文终止时,全部活动工做
线程都将终止。咱们也可使用terminate()方法显式的终止worker,这将显式中止线程而无需等待任何
现有代码执行完成。可是,不多去显式终止worker。一旦建立,workers一般在页面整个生命周期内存活。
生成worker不是免费的,它会产生开销,因此若是可能的话,咱们应该只作一次。

共享应用状态

在本节中,咱们将介绍共享workers。首先,咱们将了解多个浏览上下文如何访问内存中的相同数据对象。而后,咱们将介绍如何获取远程资源,以及如何通知多个浏览上下文有关新数据的返回。最后,咱们将了解如何利用共享workers来容许浏览上下文之间的直接消息传递。

考虑下本节用于实验编码的高级特性。浏览器对共享workers的支持目前还不是很好(只有Firefox和Chrome)。
Web worker仍处于W3C的候选推荐阶段。一旦他们成为推荐并为共享workers提供了更好的浏览器支持,
咱们就可使用它们了。对于额外的意义,当服务workers规范成熟,共享Worker能力将更加剧要。

共享内存

到目前为止咱们已经看到了Web workers的序列化机制,由于咱们不能直接从多个线程引用同一个对象。可是,共享worker的内存空间不只限于一个页面,这意味着咱们能够经过各类消息传递方法间接访问内存中的这些对象。实际上,这是一个展现咱们如何使用端口传递消息的好机会。让咱们来看看吧。

端口的概念对于共享worker是很必要的。没有它们,就没有管理机制来控制来自共享worker的消息的流入和流出。例如,假设咱们有三个页面使用相同的共享worker,那么咱们必须建立三个端口来与该workers通讯。将端口视为workers通往外部世界的入口。这是一个小的间接的过程。

这是一个基本的共享worker,让咱们了解设置这些类型的workers所涉及的内容:

//这是链接到worker的页面之间的共享状态数据
var connections = 0;

//侦听链接到此worker的页面,
//咱们能够设置消息端口。
addEventListener('connect', (e) => {
    //“source”属性表明由链接到这个worker页面建立的消息端口,
    //咱们实际上要经过调用“start()”创建链接。
    e.source.start();
});

//咱们将消息发回页面,数据是更新的链接数。
e.source.postMessage(++connections);

一旦页面与此worker链接,就会触发一个connect事件。该connect事件具备一个source属性,这是消息端口。咱们必须经过调用start()来告诉这个worker已准备开始与它通讯。请注意,咱们必须在端口上调用postMessage(),而不是在全局上下文中调用。worker怎么知道要将消息发送到哪一个页面?该端口充当worker和页面之间的代理,以下图所示:

image109.gif

如今让咱们看看如何在多个页面中使用这个共享worker:

//启动共享worker。
var worker = new SharedWorker('worker.js');

//设置“message”事件处理程序。
//经过链接共享worker,咱们其实是在建立一个消息
//发送到消息传递端口。
worker.port.addEventListener('message', (e) => {
    console.log('connections made', e.data);
});

//启动消息传递端口,
//代表咱们是准备开始发送和接收消息。
worker.port.start();

这个共享worker和专用worker之间只有两个主要区别。它们以下:

• 咱们有一个port对象,咱们能够经过发布消息和附加事件监听器来与worker通讯。

• 咱们告诉worker咱们已准备好经过调用端口上的start()方法来启动通讯,就像worker同样。

将这两个start()调用视为共享worker与其客户端之间的握手。

获取资源

前面的示例让咱们了解了来自同一应用程序的不一样页面如何共享数据,从而无需在加载页面时分配两次彻底相同的结构。让咱们以这个方法为基础,使用共享worker来获取远程资源,以便与任何依赖它的页面共享返回的结果。这是worker线程代码:

//咱们保存链接页面的端口,
//以便咱们能够广播消息。
var ports = [];

//从API获取资源。
function fetch() {
    var request = new XMLHttpRequest();
    
    //当接口响应时,咱们只需解析JSON字符串一次,
    //而后将它广播到全部端口。
    request.addEventListener('load', (e) => {
        var resp = JSON.parse(e.target.responseText);
        for (let port of ports) {
            port.postMessage(resp);
        }
    });

    request.open('get', 'api.json');
    request.send();
}

//当一个页面链接到这个worker时,
//咱们保存到“ports”数组,
//以便worker能够持续跟踪它。
addEventListener('connect', (e) => {
    ports.push(e.source);
    e.source.start();
});

//如今咱们能够“poll”API,并广播结果到全部页面。
setInterval(fetch, 1000);

咱们只是在ports数组中存储对它的引用,而不是在页面链接到worker时响应端口。这就是咱们如何跟踪链接到worker页面的方式,这很重要,由于并不是全部消息都遵循命令响应模式。在这种状况下,咱们但愿将更新的API资源广播到正在监听它的全部页面。一个常见的状况是在同一个应用程序,若是有许多浏览器选项卡打开查看同一个页面,咱们可使用相同的数据。

例如,若是API资源是一个很大的JSON数组须要被解析,若是三个不一样的浏览器选项卡解析彻底相同的数据,则会很浪费资源。另外一个好处是咱们不会轮询API 3次,若是每一个页面都运行本身的轮询代码就会是这种状况。当它在共享worker上下文中时,它只发生一次,而且数据被分发到链接的页面。这对后端的负担也较少,由于整体而言,发起的请求要少得多。咱们如今来看看这个worker使用的代码:

//启动共享worker
var worker = new SharedWorker('worker.js');

//监听“message”事件,
//并打印从worker发回的任何数据。
worker.port.addEventListener('message', (e) => {
    console.log('from worker', e.data);
});

//通知共享worker咱们已经准备好了开始接收消息
worker.port.start();

在页面间进行通讯

到目前为止,咱们已经处理过以共享worker中的数据为中心的数据资源。也就是说,它来自于一个集中的地方,好比做为一个API,随后页面经过链接worker来读取数据。咱们实际上没有从页面修改任何的数据。例如,咱们甚至没有链接到后端,链接共享worker的页面也没有产生任何数据。如今其余页面都须要知道这些改变。

可是,让咱们说用户切换到其中一个页面并进行一些调整。咱们必须支持双向更新。让咱们来看看如何使用共享worker来实现这些功能:

//保存全部链接页面的端口。
var ports = [];

addEventListener('connect', (e) => {

    //收到的消息数据被分发给任何链接到此worker的页面。
    //页面代码逻辑决定如何处理数据。
    e.source.addEventListener('message', (e) => {
        for (let port of ports) {
            port.postMessage(e.data);
        }
    });
});

//保存链接页面的端口引用,
//使用“start()”方法开始通讯。
ports.push(e.source);
e.source.start();

这个worker就像是一颗卫星; 它只是将收到的全部内容传输到已链接的端口。这就是咱们所须要的,为何还须要更多?咱们来看看链接到这个worker的页面代码:

//启动共享worker,
//并保存咱们正在使用的UI元素的引用。
var worker = new SharedWorker('worker.js');
var input = document.querySelector('input');

//每当输入值改变时,发送输入值数据
//到worker以供其余须要的页面使用。
input.addEventListener('input', (e) => {
    worker.port.postMessage(e.target.value);
});

//当咱们收到输入数据时,更新咱们文字输入框的值,
//也就是说,除非值已经更新。
worker.port.addEventListener('message', (e) => {
    if (e.data !== input.value) {
        input.value = e.data;
    }
});

//启动worker开始通讯。
worker.port.start();

有趣!如今,若是咱们继续打开两个或更多浏览器选项卡,咱们对输入值的任何更改都将当即反映在其余页面中。这个设计的优势在于它的表现一致; 不管哪一个页面执行更新,任何其余页面都会收到更新的数据。换句话说,这些页面承担着数据生产者和数据消费者的双重角色。

您可能已经注意到,最后一个示例中的worker向全部端口发送消息,包括发送消息的端口。咱们确定不想这样作。
为避免向发送方发送消息,咱们须要以某种方式排除for..of循环中的发送端口。

这实际上并不容易,由于消息事件对象没有与一块儿发送端口的识别信息。咱们能够创建端口标识符并使​​消息包含ID。
这里须要有不少工做,好处并非那么好。这里的并发设计 - 只是简单地检查页面代码,该消息实际上与页面相关。

经过子workers执行子任务

咱们在本章中建立的全部workers - 专用workers和共享workers - 都是由主线程生成的。在本节中,咱们将讨论子workers。它们与专用worker类似,只是建立者不一样。例如,子worker不能直接与主线程交互,只能经过产生子workers的代理进行交互。

咱们将看看将较大的任务划分为较小的任务,而且咱们还将看看围绕子worker的一些挑战性问题。

将工做分为任务

咱们的Web worker的工做是以这样的方式执行任务,即主线程能够继续服务于一些事情,例如DOM事件,而不会中断。对于Web worker线程来讲,某些任务很简单。它们接受输入,计算结果,并将结果做为输出返回。可是,若是任务很复杂,该怎么办?若是它涉及许多较小的分散步骤,须要咱们将较大的任务分解为较小的任务,该怎么办?

像这些任务,经过将它们分解为更小的子任务是有意义的,这样咱们就能够进一步利用全部可用的CPU。然而,将任务分解为较小的任务自己会致使严重的性能损失。若是任务分解放在主线程中,咱们的用户体验可能会受到影响。咱们在这里使用的一种技术涉及启动一个Web worker,其工做是将任务分解为更小的步骤,并为每一个步骤启动子worker。

让咱们建立一个在数组中搜索指定项的worker,若是该项存在则返回true。若是输入数组很大,咱们会将它分红几个较小的数组,每一个数组都是并行搜索的。这些并行搜索任务将做为子worker建立。首先,咱们来看看子worker:

//侦听传入的消息。
addEventListener('message', (e) => {
    
    //将结果发回给worker。
    //咱们在输入数组上调用“indexOf()”,寻找“search”数据。
    postMessage({
        result: e.data.array.indexOf(e.data.search) > -1
    });
});

因此,咱们如今有一个子worker能够获取一个数组的块并返回一个结果。这很简单。如今,对于棘手的部分,让咱们实现将输入数组划分为较小输入的worker,而后将其输入子worker。

addEventListener('message', (e) => {

    //咱们将要分红4个较小块的数组。
    var array = e.data.array;

    //大体计算数组四分之一的大小,
    //这将是咱们的块大小。
    var size = Math.floor(0.25 * array.length);

    //咱们正在寻找的搜索数据。
    var search = e.data.search;

    //用于在下面的“while”循环将数组分红块。
    var index = 0;

    //一旦被切片,咱们的块就会去执行。
    var chunks = [];

    //咱们须要保存对子worker的引用,
    //这样咱们能够终止它们。
    var workers = [];

    //这用于统计从子workers返回的结果数
    var results = 0;

    //将数组拆分为按比例大小的块。
    while (index < array.length) {
        chunks.push(array.slice(index, index + size));
        index += size;
    }

    //若是还有剩下的(第5块),
    //把它放到它以前的块中。
    if (chunks.length> 4) {
        chunks[3] = chunks[3].concat(chunks[4]);
        chunks = chunks.slice(0, 4);
    }

    for (let chunk of chunks) {
        
        //启动咱们的子worker并在“workers”中保存它的引用。
        let worker = new Worker('sub-worker.js');
        workers.push(worker);

        //当子worker有返回结果时。
        worker.addEventListener('message', (e) => {
            results++;

            //结果是“truthy”,咱们能够发送一个响应给主线程。
            //不然,咱们检查是否所有子workers都返回了。
            //若是是这样,咱们能够发送一个false返回值。
            //而后,终止全部子workers。
            if (e.data.result) {
                postMessage({
                    search: search,
                    result: true
                });
                
                workers.forEach(x => x.terminate());
            } else if (results === 4) {
                postMessage({
                    search: search,
                    result: false
                });

                workers.forEach(x => x.terminate());
            }
        });

        //为worker提供一大块数组进行搜索。
        worker.postMessage({
            array: chunk,
            search: search
        });
    }
});

这种方法的优势是,一旦咱们获得了正确的结果,咱们就能够终止全部现有的子worker。所以,若是咱们执行一个特别大的数据集,就能够避免让一个或多个子worker在后台进行没必要要的运算。

咱们在这里采用的方法是将输入数组切成四个比例(25%)的块。这样,咱们将并发级别限制为四级。在下一章中,咱们将进一步讨论细分任务和技巧,以肯定要使用的并发级别。

如今,让咱们经过编写一些代码在页面上使用这个worker以完成示例:

//启动worker...
var worker = new Worker('worker.js');

//生成一些输入数据,一个数字0 - 1041数组。
var input = new Array(1041).fill(true).map((v, i) => i);

//当worker返回时,显示咱们搜索的结果。
worker.addEventListener('message', (e) => {
    console.log(`${e.data.search} exists?`, e.data.result);
});

//搜索一个存在的项。
worker.postMessage({
    array: input,
    search: 449
});
//→449存在?真

//搜索一个不存在的项。
worker.postMessage({
    array: input,
    search: 1045
});
//→1045存在?假

咱们可以与worker通讯,传递输入数组和数据进行搜索。结果传递给主线程,它们包含搜索词,所以咱们可以经过发送给worker线程的原始消息对输出进行协调。然而,这里有一些困难须要克服。虽然这很是有用,可以细分任务以更好地利用多核CPU,但涉及到不少复杂性。一旦咱们获得每一个子worker的结果,咱们就必须进行协调。

若是这个简单的例子能够变得像它同样复杂,那么想象一下大型应用程序的上下文中的相似代码。咱们能够从两个角度解决这些并发问题。首先是关于并发的前期设计挑战。这将在下一个章节解决。而后,还有是同步挑战,咱们如何避免回调地狱?这个话题比较深,将在“第7章,抽取并发逻辑”讨论。

提醒一下

虽然前面的示例是一种强大的并发技术,能够提供很大的性能提高,但还有一些问题须要注意。所以,在深刻涉及子worker的实现以前,请考虑其中的一些挑战以及必须作出的权衡。

子workers没有一个父页面来直接通讯。这是一个复杂的设计,由于即便一个来自子worker简单响应也须要子worker经过代理从而在运行的JavaScript主线程进行建立。而这样作获得的是一堆让人困惑的通讯过程。换句话说,它很容易致使复杂化的设计,由于要经过比实际上须要的更多组件来完成。因此,在决定使用子workers做为设计选项以前,让咱们看看是否能够只依赖于专用worker来实现。

第二个问题是,因为Web worker仍然是候选推荐的W3C规范,并不是全部浏览器都能一致的实现Web worker的全部功能。共享workers和子workers是咱们可能遇到跨浏览器问题的两个部分。另外一方面,专用workers具备很好的浏览器支持,而且在大部分浏览器中表现一致。再一次说明,从简单的专用worker设计开始,若是这不知足须要,再考虑引入共享workers和子workers。

Web workers中的错误处理

本章中的全部代码都假设咱们的worker程序中运行的代码不会出错。显然,咱们的workers会遇到异常被抛出的状况,或者是咱们在开发过程当中编写有bug的代码 - 这是咱们做为程序员所必须面临的事实。可是,若是没有适当的错误事件处理程序,Web worker可能很难调试。咱们能够采起的另外一种方法是显式发回一条消息,标识本身已经出错。咱们将在本节中介绍两个错误处理话题。

错误条件检查

假设咱们的主应用程序代码向worker线程发送消息,并指望获得一些返回结果。若是出现问题,那么等待数据的代码须要知道该怎么办?一种可能性是仍然发送主线程指望的消息; 只是它有一个字段表示操做错误的状态。下图让咱们了解下它是怎么样的:

image115.gif

如今让咱们看一下实现这种方法的代码。首先,worker肯定消息返回成功或错误状态:

//当消息返回时,检查提供的消息数据是不是一个数组。
//若是不是,返回一个设置了“error”属性的数据。
//不然,计算并返回结果。
addEventListener('message', (e) => {
    if(!Array.isArray(e.data)) {
        postMessage({
            error: 'expecting an array'
        });
    } else {
        postMessage({
            error: e.data[0]
        });
    }
});

该worker老是会经过发送一个消息进行响应,但它并不老是返回一个计算结果。首先,它会检查,以确保该输入值是能够接受的。若是没有获得指望的数据,它发送一个附加错误状态的消息。不然,它正常的发送返回结果。如今,让咱们编写一些代码来使用这个worker:

//启动worker
var worker = new Worker('worker.js');

//监听来自worker的消息。
//若是收到错误,咱们会记录错误信息。
//不然,咱们记录成功的结果。
worker.addEventListener('message', (e) => {
    if (e.data.error) {
        console.error(e.data.error);
    } else {
        console.log('result', e.data.result);
    }
});

worker.postMessage([3, 2, 1]);
//→result 3

worker.postMessage({});
//→expecting an array

异常处理

即便咱们在上一个示例中明确检查了workers程序中的错误状况,也可能会抛出异常。从咱们的主应用程序线程的角度来看,咱们须要处理这些未捕获类型的错误。若是没有适当的错误处理机制,咱们的Web workers将悄然无声地失败。有时候,workers甚至都不加载 - 遇到这种悄无声息的代码调试。

咱们来看一个侦听Web worker error事件的示例。这是一个Web worker尝试访问不存在的属性:

//当一个消息数组返回时,
//发送一个包含的“name”属性输入数据做为响应,
//若是数据没有定义怎么办?
addEventListener('message', (e) => {
    postMessage(e.data.name);
});

这里没有错误处理代码。咱们所作的只是经过读取name属性并将其发回来做为响应消息。让咱们看一下使用这个worker的一些代码,以及它如何响应这个worker中引起的异常:

//启动咱们的worker
var worker = new Worker('worker.js');

//监遵从worker发回的消息,
//并打印结果数据。
worker.addEventListener('message', (e) => {
    console.log('result', `"${e.data}"`);
});

//监遵从worker发回的错误,
//并打印错误消息。
worker.addEventListener('error', (e) => {
    console.error(e.message);
});

worker.postMessage(null);
//→Uncaught TypeError:Cannot read property "name" of null

worker.postMessage({name: 'JavaScript'});
//→result "JavaScript"

在这里,咱们能够看到的是第一个发布消息的worker致使异常被抛出。然而,此异常被封装在worker内部,它不是抛出在咱们的主线程。若是咱们在主线程监听error事件,咱们就能够作出相应的响应。在这里,咱们只是打印错误消息。然而,在其余状况下,咱们可能须要采起更复杂的纠正措施,例如释放资源或发送一个不一样的消息给worker。

小结

在本章中,咱们介绍了使用Web worker并发执行的概念。在Web worker以前,咱们的JavaScript没法利用当今硬件上的多核CPU。

咱们首先对Web worker进行了大体的概述。它们是操做系统级的线程。从JavaScript的角度来看,它们是能够发送消息和监听message事件的事件对象。Web worker主要分为三种 - 专用workers,共享workers和子workers。

而后,学习了如何经过发送消息和监听事件来与Web worker进行通讯。而且了解到,在消息中传递的内容方面存在限制。这是由于全部消息数据都在目标线程中被序列化和重建。

咱们以如何处理Web worker中的错误和异常来结束本章。在下一章中,咱们将讨论并发的实际应用 - 咱们应该使用并行执行的任务类型,以及实现它的最佳方法。

最后补充下书籍章节目录

另外还有讲解两章nodeJs后端并发方面的,和一章项目实战方面的,这里就再也不贴了,有兴趣可转向https://github.com/yzsunlei/javascript_concurrency_translation查看。

相关文章
相关标签/搜索