[书籍翻译] 《JavaScript并发编程》第七章 抽取并发逻辑

本文是我翻译《JavaScript Concurrency》书籍的第七章 抽取并发逻辑,该书主要以Promises、Generator、Web workers等技术来讲解JavaScript并发编程方面的实践。

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

到本书这里,我们已经在代码中明确地模拟了并发问题。使用promises,我们同步化了两个或更多异步操作。使用生成器,我们按需创建数据,避免不必要的内存分配。最后,我们了解到Web worker是利用多个CPU内核的主要工具。

在本章中,我们将采用所有这些方法并将它们放入应用程序代码的上下文中。也就是说,如果并发是默认的,那么我们需要使并发尽可能不那么明显。我们将首先探索各种技术,这些技术将帮助我们在使用的组件中封装并发机制。然后,我们将通过使用promises来帮助worker通信,直接改进前两章的代码。

一旦我们能够使用promises抽象worker通信,我们将尝试在生成器的帮助下实现惰性的worker。我们还将使用Parallel.js库来介绍worker抽象,然后是worker线程池的概念。

编写并发代码

并发编程很难做到。即使是人为的示例应用程序,大部分复杂性来自并发代码。我们显然希望我们的代码可读性好,同时保持并发的好处。我们希望充分利用系统上的每个CPU。我们只想在需要时计算我们需要的东西。我们不希望意大利面条式的代码将多个异步操作混在一起。在开发应用程序的同时关注并发编程的所有这些方面会削弱我们应该关注的内容 – 提供应用程序有价值的功能。

在本节中,我们将介绍可能用于将我们的应用程序的其余部分与棘手的并发隔离的方法。这通常意味着将并发作为默认模式 – 即使在引擎下没有发生真正的并发时也是如此。最后,我们不希望我们的代码包含90%的并发处理技巧,而只有10%的功能。

隐藏并发机制

在我们所有的代码中暴露并发机制的难度是,他们每一个都稍微不同于另一个。这扩大了我们可能已经发现所在的回调地狱的情况。例如,不是所有的并发操作都是从一些远程资源获取数据的网络请求。异步数据可能来自一个worker或一些本身就是异步的回调。想象一下场景我们使用了三个不同的数据源来计算一个我们需要的值,所有的这些都是异步的。这里是这个问题的示图:

《[书籍翻译] 《JavaScript并发编程》第七章 抽取并发逻辑》

此图中的数据是我们在应用程序代码中关注的内容。从我们正在构建的功能的角度来看,我们并不关心上述任何事情。因此,我们的前端架构需要封装与并发相关的复杂性。这意味着我们的每个组件都应该能够以相同的方式访问数据。除了我们所有的异步数据源之外,还有另一个要考虑的复杂因素 – 当数据不是异步的并且来自本地数据源呢?那么同步本地数据源和HTTP请求呢?我们将在下一节中介绍这些。

没有并发性

仅仅因为我们正在编写并发JavaScript应用程序,并非每个操作本身都是并发的。例如,如果一个组件向另一个组件询问它已经在内存中的数据,则它不是异步操作并会立即返回。我们的应用程序可能到处都是这些操作,其中并发性根本就没有意义。其中存在的挑战 – 我们如何将异步操作与同步操作无缝混合?

简单的答案是我们在每处做出并发的默认假设。promise使这个问题易于处理。以下是使用promise来封装异步和同步操作的示图说明:

《[书籍翻译] 《JavaScript并发编程》第七章 抽取并发逻辑》

这看起来很像前面的那个图,但有两个重要区别。我们添加了一个synchronous()操作; 这没有回调函数,因为它不需要回调函数。它不是在等待其他任何东西,所以它会直接地返回。其他两个函数就像在上图中一样;两者都依赖回调函数将数据提供给我们的应用程序。第二个重要区别是有一个promise对象。这取代了sync()操作和数据概念。或者更确切地说,它将它们融合到同一个概念中。

这是promise的关键作用 – 它们为我们抽象同步问题提供能力。这不仅适用于网络请求,还适用于Web worker消息或依赖于回调的任何其他异步操作。它需要一些调整来考虑下我们的数据,因为我们得保证它最终会到达这里。但是,一旦我们消除了这种心理差距,默认情况下就会启用并发。就我们的功能而言,并发是默认的,而我们在操作背后所做的事情并不是最具破坏性的。

现在让我们看一些代码。我们将创建两个函数:一个是异步的,另一个是简单返回值的普通函数。这里的目标是使运行这些函数的代码相同,尽管生成值的方式有很大不同:

//一个异步“fetch”函数。我们使用“setTimeout()”
//在1秒后通过“callback()”返回一些数据。
function fetchAsync(callback) {
    setTimeout(() => {
        callback({hello