ArrayBuffers 和 SharedArrayBuffers 的卡通介绍

这是三篇系列文章中的第二篇

  1. 内存管理速成课程
  2. ArrayBuffers 和 SharedArrayBuffers 的卡通介绍
  3. 使用 Atomics 避免 SharedArrayBuffers 中的竞态条件

上一篇文章 中,我解释了 JavaScript 等内存管理型语言如何处理内存。我还解释了 C 等语言中的手动内存管理工作原理。

当我们谈论 ArrayBuffersSharedArrayBuffers 时,为什么这很重要呢?

因为 ArrayBuffers 为你提供了一种手动处理部分数据的机制,即使你是在使用具有自动内存管理功能的 JavaScript。

为什么你想要这样做呢?

正如我们在上一篇文章中讨论的那样,自动内存管理存在权衡。它对开发者来说更简单,但会增加一些开销。在某些情况下,这种开销会导致性能问题。

A balancing scale showing that automatic memory management is easier to understand, but harder to make fast

例如,当你用 JS 创建变量时,引擎必须猜测这是一种什么样的变量以及它在内存中的表示方式。由于它是猜测,因此 JS 引擎通常会为变量保留比实际需要的更多的空间。根据变量的不同,内存槽的大小可能是其实际需要的 2-8 倍,这会导致大量内存浪费。

此外,创建和使用 JS 对象的某些模式会使垃圾回收变得更加困难。如果你在进行手动内存管理,则可以选择适合你所处理的用例的分配和释放策略。

大多数情况下,这并不值得麻烦。大多数用例对性能并不敏感,因此你无需担心手动内存管理。对于常见用例,手动内存管理甚至可能更慢。

但是,在你需要在低级别工作以使代码尽可能快的时候,ArrayBuffers 和 SharedArrayBuffers 为你提供了选择。

A balancing scale showing that manual memory management gives you more control for performance fine-tuning, but requires more thought and planning

那么 ArrayBuffer 是如何工作的呢?

它基本上就像使用其他任何 JavaScript 数组一样。不同的是,当你使用 ArrayBuffer 时,你不能在其中放入任何 JavaScript 类型,例如对象或字符串。你唯一可以放入的是字节(可以使用数字表示)。

Two arrays, a normal array which can contain numbers, objects, strings, etc, and an ArrayBuffer, which can only contain bytes

在这里我需要澄清一点,你实际上并没有直接将此字节添加到 ArrayBuffer 中。就其本身而言,此 ArrayBuffer 不知道字节应该有多大,也不知道不同类型的数字应该如何转换为字节。

ArrayBuffer 本身只是一堆零和一排成一行。ArrayBuffer 不知道应该在数组中第一个元素和第二个元素之间进行何种划分。

A bunch of ones and zeros in a line

为了提供上下文,为了真正将这些内容拆分为多个框,我们需要用一个称为视图的内容对其进行包装。这些数据视图可以使用类型化数组添加,并且可以使用它们与许多不同类型的类型化数组进行交互。

例如,你可以使用 Int8 类型化数组,它将把这些内容拆分为 8 位字节。

Those ones and zeros broken up into boxes of 8

或者,你可以使用无符号 Int16 数组,它会将其拆分为 16 位字节,并将其视为无符号整数进行处理。

Those ones and zeros broken up into boxes of 16

你甚至可以对同一个基础缓冲区使用多个视图。对于相同的操作,不同的视图将为你提供不同的结果。

例如,如果我们从 ArrayBuffer 上的 Int8 视图中获取元素 0 和 1,它会给我们不同的值,而不是 Uint16 视图中元素 0 的值,即使它们包含完全相同的位。

Those ones and zeros broken up into boxes of 16

这样,ArrayBuffer 基本上就像原始内存一样。它模拟了你可以在 C 等语言中使用的直接内存访问方式。

你可能想知道为什么我们不直接给程序员提供对内存的直接访问权限,而是添加了这一层抽象。直接访问内存会产生一些安全漏洞。我将在以后的文章中对此进行更多解释。

那么,SharedArrayBuffer 是什么呢?

为了解释 SharedArrayBuffers,我需要解释一下在并行和 JavaScript 中运行代码。

你会并行运行代码以使你的代码运行得更快,或者使它对用户事件的响应更快。为此,你需要将工作分成多个部分。

在一个典型的应用程序中,所有的工作都由一个单一的人员负责——主线程。我之前谈过这个……主线程就像一个全栈开发者。它负责 JavaScript、DOM 和布局。

你为减轻主线程的工作负担所做的任何事情都有帮助。在某些情况下,ArrayBuffers 可以减少主线程需要做的工作量。

The main thread standing at its desk with a pile of paperwork. The top part of that pile has been removed

但有时仅仅减轻主线程的工作负担还不够。有时你需要带来增援力量……你需要将工作分成多个部分。

在大多数编程语言中,你通常使用一种称为线程的内容来将工作分成多个部分。这基本上就像让几个人一起完成一个项目。如果你有一些彼此相对独立的任务,你可以将它们分配给不同的线程。然后,这两个线程可以在同一时间分别处理各自的任务。

在 JavaScript 中,你可以使用称为 Web Worker 的内容来做到这一点。这些 Web Worker 与你在其他语言中使用的线程略有不同。默认情况下,它们不共享内存。

Two threads at desks next to each other. Their piles of paperwork are half as tall as before. There is a chunk of memory below each, but not connected to the other's memory

这意味着如果你想与另一个线程共享一些数据,你就必须将其复制过去。这可以使用 postMessage 函数完成。

postMessage 会获取你放入其中的任何对象,将其序列化,将其发送到另一个 Web Worker,在那里它会被反序列化并放入内存。

Thread 1 shares memory with thread 2 by serializing it, sending it across, where it is copied into thread 2's memory

这是一个相当缓慢的过程。

对于某些类型的数据,例如 ArrayBuffers,你可以执行所谓的内存传输。这意味着将该特定内存块移动过去,以便另一个 Web Worker 可以访问它。

但之后第一个 Web Worker 就无法再访问它了。

Thread 1 shares memory with thread 2 by transferring it. Thread 1 no longer has access to it

这在某些用例中有效,但在许多情况下,如果你想要实现这种高性能并行性,你真正需要的是共享内存。

这就是 SharedArrayBuffers 为你提供的功能。

The two threads get some shared memory which they can both access

使用 SharedArrayBuffer,两个 Web Worker、两个线程都可以写入和读取同一内存块中的数据。

这意味着它们没有使用 postMessage 会产生的通信开销和延迟。两个 Web Worker 都可以立即访问数据。

不过,从两个线程同时进行这种立即访问存在一些危险。它会导致所谓的竞态条件。

Drawing of two threads racing towards memory

我将在 下一篇文章 中对此进行更多解释。

SharedArrayBuffers 的当前状态如何?

SharedArrayBuffers 很快将出现在所有主流浏览器中。

Logos of the major browsers high-fiving

它们已经包含在 Safari 中(在 Safari 10.1 中)。Firefox 和 Chrome 都将在 7 月/8 月的发布版本中提供它们。Edge 计划将其包含在秋季的 Windows 更新中。

即使它们在所有主流浏览器中都可用,我们也不希望应用程序开发者直接使用它们。事实上,我们建议不要这样做。你应该使用对你来说最高级别的抽象。

我们预计 JavaScript 库开发者会创建一些库,这些库可以为你提供更轻松、更安全的方式来使用 SharedArrayBuffers。

此外,一旦 SharedArrayBuffers 被构建到平台中,WebAssembly 就可以使用它们来实现对线程的支持。一旦到位,你就可以使用 Rust 等语言的并发抽象,而 Rust 的主要目标之一就是实现无畏的并发。

下一篇文章 中,我们将重点介绍这些库作者用来构建这些抽象并同时避免竞态条件的工具 (Atomics)。

Layer diagram showing SharedArrayBuffer + Atomics as the foundation, and JS libaries and WebAssembly threading building on top

关于 Lin Clark

Lin 在 Mozilla 的高级开发部门工作,专注于 Rust 和 WebAssembly。

更多 Lin Clark 的文章…


3 条评论

  1. Julen

    非常感谢你提供了如此出色的解释,Lin。
    期待能够在现代 Web 应用程序中使用它。

    2017 年 6 月 16 日 17:09

  2. alican krlr

    感谢这些文章!我刚在 Mozilla Hacks 的主页上看到这些文章,就在我查找多线程 Node.js 讨论(例如为什么 Node.js 是单线程的等等)和 WebAssembly 的时候,这篇文章很有道理。
    https://softwareengineering.stackexchange.com/questions/315454/what-are-the-drawbacks-of-making-a-multi-threaded-javascript-runtime-implementat
    https://softwareengineeringdaily.com/2015/08/02/how-does-node-js-work-asynchronously-without-multithreading/
    https://stackoverflow.com/questions/40028377/is-it-possible-to-achieve-multithreading-in-nodejs
    https://stackoverflow.com/questions/17959663/why-is-node-js-single-threaded

    2017年6月19日 上午6:00

  3. Andrey Melikhov

    嗨!俄语版本已完成 :)
    https://medium.com/devschacht/a-cartoon-intro-to-arraybuffers-and-sharedarraybuffers-952198b0a1c9

    2017年6月26日 上午3:23

这篇文章的评论已关闭。