用 ParallelJS 并行处理 JavaScript

610 查看

伴随 HTML5 涌现出的新事物当中,最酷炫的当属 Web Workers API 的 Worker 接口。在这以前,我们必须使用一些特殊的技巧,才能向用户提供响应式的网页。而 Worker 接口能让我们创建运行周期长并且计算复杂度高的函数。而且, 通过 Worker 实例,我们想要多少 worker 就可以派生出多少。

我在这篇文章将介绍多线程的重要性,以及如何用 ParallelJS 在 JavaScript 中实现多线程。

为什么说多线程如此重要?

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

在过去的几年中,同步多线程(SMT)一直是提升现代 CPU 计算性能的关键。原因很简单:依然可以用摩尔定律中关于单位面积上晶体管数量的论断来解释。然而频率缩放却滞步不前。所以必须使用可用的晶体管。结构化的提升(比如单指令多流数据流)和多核通常是最佳方案。

为了实现同步多线程,我们需要书写并行代码,这种代码并行运行并最终得到一个结果。因为大多数顺序代码要么很难转化成并行代码,要么转换后效率非常低下,所以我们通常得用一些特殊的算法。因为根据阿姆达尔定律,速度曲线 S 的表达式如下:

其中 N 代表并行 workers (比如进程、内核或者线程)的数量,P 代表并行分片。以后可能会有更多依赖于并行算法的内核架构。在高性能计算 GPU 系统和特殊架构的领域中,比如 Intel Xeon Phi 就是这种平台的代表:

最后,我们应该区分一般的并发应用或算法与并行执行之间的区别。并行指的是(彼此可能相关的)一组计算过程的同步处理。相比而言,并发指的是一组独立执行的进程。

JavaScript 中的多线程

我们已经知道在 JavaScript 中通过回调函数实现并发程序的方法。这种方法现在也可以用来实现异步程序。

JavaScript 本身是运行在单线程上的,通过一种事件轮询机制实现(通常遵循反应器模式)。比如,我们可以利用这个特点来处理对外部资源的异步请求。这还能确保预先定义的回调函数永远只能在相同的运行线程内被触发。 JavaScript 不存在交叉线程异常、竞态条件或者其它跟线程有关的复杂问题。但尽管如此, JavaScript 并未给我们提供一个实现同步多线程的方法。

随着 Worker 接口的引入,以上难题将迎刃而解。从主要应用的角度看, web worker 的代码属于并发运行的任务。通信也是按照这种规则去处理的。 messages API 同样适合包含站点和主机页面之间的通信。

比如下面的代码在收到消息时,将向发起方响应一个消息:

虽然理论上一个 web worker 能派生出有一个 web worker ,但实际上大多数浏览器都不允许这么做。所以实现 web worker 之间通信的唯一方法就是通过 main 应用。通信中的消息并发执行,这样就实现了只有异步非阻塞的通信。最开始这种代码可能看上去有点儿奇怪,但优点还是不少的。最重要的是,这种代码不受竞态条件的限制。

这里看一个简单的例子,在后台计算一连串儿的素数,用两个参数代表序列的起点和重点。首先创建一个名为 prime.js 的文件,在文件中写入以下代码:

 

现在我们只需要在 main 应用中添加下面这段代码,就能启动后台 worker 。

 

这真是太麻烦了,尤其是还需要引入另一个文件。尽管这样能起到一个很好的分离作用,但是对于小型任务来说,这样做未免显得太累赘了。好在幸运的是我们还有一个别的方法。来看下面这段代码:

当然我们可能想实现一个比这种特殊数字( 13 和 14 )更好的方案,取决于浏览器,需要撤销 Blob 和 createObjectURL 。这里要解释一下 fs.substr(13, fs.length – 14) 的作用,它是用来提取函数体的。通过调用 toString() 方法把函数声明转化成一个字符串,并去掉函数本身的签名,这样来提取函数。

有没有一种js库能帮我们实现并行?

ParallelJS 登场

ParallelJS 现在隆重登场。 ParallelJS 为 web workers 提供了更好更方便的 API 。它包括许多辅助函数和非常有用的抽象。我们从提供一些数据来开始。

 

data 将打印出第一行给出的数组。这里还没有用到“并行”。但是实例 p 包含了一组方法,比如 spawn 方法,用来派生一个新的w web worker 。他将返回一个 Promise, 这让产生结果变得轻而易举。

上面这段代码的问题在于这个计算过程实际上并不是真正的并行。我们只是创建了一个单一的后台 worker , 这个后台 worker 一次性处理所有的数组。只有当整个数组处理完以后,才能获取结果。

更好的解决方案是使用 Parallel 实例的 map 函数。

前一个例子当中的代码非常简单,简单的让人难以置信。在实际当中,会涉及到许多操作和函数。我们可以通过 require 函数来引入函数。

reduce 函数用来帮助我们把多个结果整合到一块。它为收集子结果和一次性执行一组指令提供了一个方便的抽象。

结论

ParallelJS 为我们规避许多可能使用 web worker 时发生的问题提供了一个的非常棒的方法。此外,它还具备一个很棒的 API ,提供了一些有用的抽象和辅助函数。以后 ParallelJS 可能还会有更进一步的优化。

随着在 JavaScript 中使用同步多线程的能力,我们可能同时想要使用向量的能力。 SIMD.js 貌似提供了一种可行的方法。在不远的以后(希望不会太遥远),把 GPU 用于计算可能是一个有效的选项。 Node.js 中存在用于 CUDA (一种并行计算的架构)的封装,但是运行原生的 JavaScript 仍然存在灵活度不够的问题。

截止本文创作之前, ParallelJS 是我们处理长运行周期计算的最好方法,能充分利用多核 CPU 的强大之处。

那么你呢?你打算怎么通过 JavaScript 发挥现代硬件的强大之处?