20.1 Atomics与SharedArrayBuffer

多个上下文访问SharedArrayBuffer时,如果同时对缓冲区执行操作,可能出现资源争用问题。
Atomics API通过强制同一时刻只能对缓冲区执行一个操作,可以让多个上下文安全地读写一个SharedArrayBuffer。Atomics API是ES2017中定义的。
仔细研究会发现Atomics API非常像一个简化版的指令集架构(ISA),这并非意外。
原子操作的本质会排斥操作系统或计算机硬件通常会自动执行的优化(比如指令重新排序)。原子操作也让并发访问内存变得不可能,如果应用不当就可能导致程序执行变慢。
为此,Atomics API的设计初衷是在最少但很稳定的原子行为基础之上,构建复杂的多线程JavaScript程序。

20.1.1 SharedArrayBuffer

SharedArrayBuffer与ArrayBuffer具有同样的API。
二者的主要区别是:ArrayBuffer必须在不同执行上下文间切换,SharedArrayBuffer则可以被任意多个执行上下文同时使用。
在多个执行上下文间共享内存,意味着并发线程操作成为了可能。传统JavaScript操作对于并发内存访问导致的资源争用没有提供保护。

20.1.2 原子操作基础

任何全局上下文中都有Atomics对象,这个对象上暴露了用于执行线程安全操作的一套静态方法,其中多数方法以一个TypedArray实例(一个SharedArrayBuffer的引用)作为第一个参数,以相关操作数作为后续参数。

1.算术及位操作方法

Atomics API提供了一套简单的方法用以执行就地修改操作。
在ECMA规范中,这些方法被定义为AtomicReadModifyWrite操作。
在底层,这些方法都会从SharedArrayBuffer中某个位置读取值,然后执行算术或位操作,最后再把计算结果写回相同的位置。
这些操作的原子本质意味着上述读取、修改、写回操作会按照顺序执行,不会被其他线程中断。

2.原子读和写

浏览器的JavaScript编译器和CPU架构本身都有权限重排指令以提升程序执行效率。
正常情况下,JavaScript的单线程环境是可以随时进行这种优化的。
但多线程下的指令重排可能导致资源争用,而且极难排错。
Atomics API通过两种主要方式解决了这个问题:
❑ 所有原子指令相互之间的顺序永远不会重排。
❑ 使用原子读或原子写保证所有指令(包括原子和非原子指令)都不会相对原子读/写重新排序。这意味着位于原子读/写之前的所有指令会在原子读/写发生前完成,而位于原子读/写之后的所有指令会在原子读/写完成后才会开始。
除了读写缓冲区的值,Atomics.load()和Atomics.store()还可以构建“代码围栏”。
JavaScript引擎保证非原子指令可以相对于load()或store()本地重排,但这个重排不会侵犯原子读/写的边界。

3.原子交换

为保证连续、不间断的先读后写,Atomics API提供了两种方法:exchange()和compareExchange()。Atomics.exchange()执行简单的交换,以保证其他线程不会中断值的交换
在多线程程序中,一个线程可能只希望在上次读取某个值之后,没有其他线程修改该值的情况下,才对共享缓冲区执行写操作。
如果这个值没有被修改,这个线程就可以安全地写入更新后的值;
如果这个值被修改了,那么执行写操作将会破坏其他线程计算的值。
对于这种任务,Atomics API提供了compare-Exchange()方法。这个方法只在目标索引处的值与预期值匹配时才会执行写操作。

4.原子Futex操作与加锁

如果没有某种锁机制,多线程程序就无法支持复杂需求。
为此,Atomics API提供了模仿Linux Futex(快速用户空间互斥量,fast user-space mutex)的方法。
这些方法本身虽然非常简单,但可以作为更复杂锁机制的基本组件。

20.2 跨上下文消息

跨文档消息,有时候也简称为XDM(cross-document messaging),是一种在不同执行上下文(如不同工作线程或不同源的页面)间传递信息的能力。
例如,www.wrox.com上的页面,想要与包含在内嵌窗格中的p2p.wrox.com上面的页面通信。
在XDM之前,要以安全方式实现这种通信需要很多工作。XDM以安全易用的方式规范化了这个功能。
XDM的核心是postMessage()方法。
目的:把数据传送到另一个位置。
该方法接收3个参数:消息、表示目标接收源的字符串,和可选的可传输对象的数组(只与工作线程相关)。
第二个参数对于安全非常重要,其可以限制浏览器交付数据的目标。

  1. let iframeWindow = document.getElementById('myframe').contentWindow;
  2. iframeWindow/postMessage('一个秘密', 'http://www.wrox.com');

最后一行代码尝试向内嵌窗格中发送一条消息,而且指定了源必须是”http://www.wrox.com”。如果源匹配,那么消息将会交付到内嵌窗格;否则,postMessage()什么也不做。
这个限制可以保护信息不会因地址改变而泄露。如果不想限制接收目标,则可以给postMessage()的第二个参数传”*”,但不推荐这么做。
接收到XDM消息后,window对象上会触发message事件。
这个事件是异步触发的,因此从消息发出到接收到消息(接收窗口触发message事件)可能有延迟。
传给onmessage事件处理程序的event对象包含以下3方面重要信息:
❑ data:作为第一个参数传递给postMessage()的字符串数据。
❑ origin:发送消息的文档源,例如”http://www.wrox.com”
❑ source:发送消息的文档中window对象的代理。这个代理对象主要用于在发送上一条消息的窗口中执行postMessage()方法。如果发送窗口有相同的源,那么这个对象应该就是window对象。
接收消息之后验证发送窗口的源非常重要。和postMessage()的第二个参数可保证,数据不会意外传给未知页面一样,在onmessage事件处理程序中检查发送窗口的源,可保证数据来自正确的地方。

  1. window.addEventListener('message', (event) => {
  2. // 确保来自预期发送者
  3. if (event.origin == 'http://wrox.com') {
  4. // 对数据做一些处理
  5. processMessage(event.data);
  6. // 可选:向来源窗口发送一条消息
  7. event.source.postMessage('收到啦!', 'http://p2p.wrox.com');
  8. }
  9. });

大多数情况下,event.source是某个window对象的代理,而非实际的window对象。因此不能通过它访问所有窗口下的信息。最好只使用postMessage(),这个方法永远存在而且可以调用。
XDM有一些怪异之处:
postMessage()的第一个参数的最初实现,始终是一个字符串。后来,第一个参数改为,允许任何结构的数据传入,不过并非所有浏览器都实现了这个改变。
为此,最好就是只通过postMessage()发送字符串。
如果需要传递结构化数据,那么最好先对该数据调用JSON.stringify(),通过postMessage()传过去之后,再在onmessage事件处理程序中调用JSON.parse()。

20.3 Encoding API

Encoding API主要用于实现字符串与定型数组之间的转换。
规范新增了4个用于执行转换的全局类:TextEncoder、TextEncoderStream、TextDecoder和TextDecoderStream。

20.3.1 文本编码

Encoding API提供了两种将字符串转换为定型数组二进制格式的方法:批量编码和流编码。
把字符串转换为定型数组时,编码器始终使用UTF-8。

1.批量编码

所谓批量,指的是JavaScript引擎会同步编码整个字符串。对于非常长的字符串,可能会花较长时间。批量编码通过TextEncoder实例完成。

  1. const textEncoder = new TextEncoder();

这个实例上有一个encode()方法,该方法接收一个字符串参数,并以Uint8Array格式返回每个字符的UTF-8编码

  1. const textEncoder = new TextEncoder();
  2. const decodeText = 'foo';
  3. const encodeText = textEncoder.encode(decodeText);
  4. // f的UTF-8编码是0x66(即十进制102)
  5. // o的UTF-8编码是0x6F(即十进制111)
  6. console.log(encodeText); // Uint8Array(3) [102, 111, 111]

编码器实例还有一个encodeInto()方法,该方法接收一个字符串和目标Unit8Array,返回一个字典。
该字典包含read和written属性,分别表示成功从源字符串读取了多少字符,和向目标数组写入了多少字符。
如果定型数组的空间不够,编码就会提前终止,返回的字典会体现这个结果。
encode()要求分配一个新的Unit8Array, encodeInto()则不需要。对于追求性能的应用,这个差别可能会带来显著不同。

2.流编码

TextEncoderStream其实就是TransformStream形式的TextEncoder。
将解码后的文本流通过管道输入流编码器会得到编码后文本块的流

20.3.2 文本解码

Encoding API提供了两种将定型数组转换为字符串的方式:批量解码和流解码。
默认字符编码格式是UTF-8。

1.批量解码

所谓批量,指的是JavaScript引擎会同步解码整个字符串。
对于非常长的字符串,可能会花较长时间。
批量解码通过TextDecoder实例完成。这个实例上有一个decode()方法,该方法接收一个定型数组参数,返回解码后的字符串。

2.流解码

TextDecoderStream其实就是TransformStream形式的TextDecoder。
将编码后的文本流通过管道输入流解码器会得到解码后文本块的流。
文本解码器流经常与fetch()一起使用,因为响应体可以作为ReadableStream来处理。