跳到主要内容

语法特性-ArrayBuffer

原文链接: 阮一峰的 ES6 入门

ArrayBuffer 是很早就存在一种数据接口, ES6 开始将其正式纳入 ECMAScript 规格. 用于处理二进制的数据.

二进制数据有三类对象:

  1. ArrayBuffer对象: 代表内存中的一段二进制数据, 可以通过"视图"进行操作, "视图"部署了数组接口, 所以可以用数组的方法来操作内存.
  2. TypedArray视图: 包含了 9 种类型的视图, Uint8Array(无符号 8 位整数), int16Array(16 位数组接口),Float32Array(32 位浮点数)等等
  3. DataView视图: 可以自定义符合格式的视图. 比如第一个字节是Unit8, 第二, 三个字节是Int16,等等, 此外还可以自定义字节序.

1. 使用

ArrayBuffer不能直接读写, 只能通过视图来读写, 视图的作用就是以指定的格式来解读二进制数据.

//创建一段32字节的内存区域, 每个字节的值默认为0
const buf = new ArrayBuffer(32);
//创建DataView视图, 以无符号8位整数从偷渡去8位二进制数据, 结果得到0.
const dataView = new DataView(buf);
dataView.getUint8(0); // 0

另一种是TypeArray视图, 与DataView视图的一个区别是, 它不是一个构造函数, 而是一组构造函数, 代表不同的数据格式:

const buffer = new ArrayBuffer(12);
//32位有符号整数类型
const x1 = new Int32Array(buffer);
x1[0] = 1;
//8位无符号整数类型
const x2 = new Uint8Array(buffer);
x2[0] = 2;

x1[0]; // 2

两个视图对应的是同一段内存, 一个视图修改底层内存, 会影响另一个视图.

TypedArray视图的构造函数, 除了接受ArrayBuffer实例, 还可以接受普通数组, 直接分配内存生成底层的ArrayBuffer实例, 并同时赋值:

const typedArray = new Uint8Array([0, 1, 2]);
typedArray.length; // 3

typedArray[0] = 5;
typedArray; // [5, 1, 2]

2. 字节序

字节序指的是数值在内存中的表示方式.

const buffer = new ArrayBuffer(16);
const int32View = new Int32Array(buffer);

for (let i = 0; i < int32View.length; i++) {
int32View[i] = i * 2;
}

上面的代码中ArrayBuffer有 16 个字节, 在他的基础上, 建立了一个 32 位整数的视图, 每个 32 位的整数占据 4 个字节, 所以可以写入 4 个整数, 依次为 0,2,4,6.

如果在这段数据上建立一个 16 位整数, 则读取的结果会完全不同:

const int16View = new Int16Array(buffer);

for (let i = 0; i < int16View.length; i++) {
console.log('Entry ' + i + ': ' + int16View[i]);
}
// Entry 0: 0
// Entry 1: 0
// Entry 2: 2
// Entry 3: 0
// Entry 4: 4
// Entry 5: 0
// Entry 6: 6
// Entry 7: 0

由于每个 16 位整数占据 2 个字节, 所以整个ArrayBuffer现在被分成了 8 段吗然后由于 x86 体系的计算机都采用小端字节序, 权重高的字节排在其后面的内存, 权重轻的字节排在前面的内存地址, 所以就得到了上面的结果.

DataView视图可以设定字节序. 为一些大端字节序的设备和系统作出调整.

// 假定某段buffer包含如下字节 [0x02, 0x01, 0x03, 0x07]
const buffer = new ArrayBuffer(4);
const v1 = new Uint8Array(buffer);
v1[0] = 2;
v1[1] = 1;
v1[2] = 3;
v1[3] = 7;

const uInt16View = new Uint16Array(buffer);

// 计算机采用小端字节序
// 所以头两个字节等于258
if (uInt16View[0] === 258) {
console.log('OK'); // "OK"
}

// 赋值运算
uInt16View[0] = 255; // 字节变为[0xFF, 0x00, 0x03, 0x07]
uInt16View[0] = 0xff05; // 字节变为[0x05, 0xFF, 0x03, 0x07]
uInt16View[1] = 0x0210; // 字节变为[0x05, 0xFF, 0x10, 0x02]

下面的代码可以判断当前的视图是小端字节序还是大端字节序:

const BIG_ENDIAN = Symbol('BIG_ENDIAN');
const LITTLE_ENDIAN = Symbol('LITTLE_ENDIAN');

function getPlatformEndianness() {
let arr32 = Uint32Array.of(0x12345678);
let arr8 = new Uint8Array(arr32.buffer);
switch (arr8[0] * 0x1000000 + arr8[1] * 0x10000 + arr8[2] * 0x100 + arr8[3]) {
case 0x12345678:
return BIG_ENDIAN;
case 0x78563412:
return LITTLE_ENDIAN;
default:
throw new Error('Unknown endianness');
}
}

2.1 BYTES_PER_ELEMENT

该属性可以表示这种数据类型占据的字节数:

Int8Array.BYTES_PER_ELEMENT; // 1
Uint8Array.BYTES_PER_ELEMENT; // 1
Uint8ClampedArray.BYTES_PER_ELEMENT; // 1
Int16Array.BYTES_PER_ELEMENT; // 2
Uint16Array.BYTES_PER_ELEMENT; // 2
Int32Array.BYTES_PER_ELEMENT; // 4
Uint32Array.BYTES_PER_ELEMENT; // 4
Float32Array.BYTES_PER_ELEMENT; // 4
Float64Array.BYTES_PER_ELEMENT; // 8

也可以直接在TypedArray中获取到:TypedArray.prototype.BYTES_PER_ELEMENT

3. 溢出

不同的视图类型, 内存空间是确定的. 超出范围的数据就会出现溢出. 比如, 8 位视图就只能容纳一个 8 位的二进制值, 如果放入一个 9 位的值, 就会溢出. TypedArray数组的溢出处理规则, 简单来说, 就是抛弃溢出的位, 然后按照视图类型进行解释.

const uint8 = new Uint8Array(1);

uint8[0] = 256;
uint8[0]; // 0

uint8[0] = -1;
uint8[0]; // 255

负数在计算机内部在用"2 的补码"表示, 将对应的整数值进行否运算, 然后加 1, 比如-1对应的正值为1, 进行否运算, 得到11111110, 再加上1就是补码形式:11111111. unit8按照无符号的 8 位整数解释11111111, 返回结果就是255.

一个简单的转换规则, 可以这样表示:

  • 正向溢出(overflow): 当输入值大于当前数据类型的最大值, 结果等于当前数据类型的最小值加上余值, 再减去 1.
  • 负向溢出(underflow): 当输入值小于当前数据类型的最小值, 结果等于当前数据类型的最大值减去余值的绝对值, 再加上 1.

(余值指的是取模运算的结果)

const int8 = new Int8Array(1);

int8[0] = 128;
int8[0]; // -128 (-128+(128%127)-1)

int8[0] = -129;
int8[0]; // 127 (127-(128%127)+1)

比较特殊的是Uint8ClampedArray视图的溢出规则, 它规定凡是发生正向溢出, 该值一律等于当前数据类型的最大值(255), 如果发生负向溢出, 该值一律等于当前数据类型的最小值(0).

4. ArrayBuffer 方法

4.1 ArrayBuffer.prototype.byteLength

ArrayBuffer实例的byteLength属性, 返回所分配内存区域的自节长度.

const buffer = new ArrayBuffer(32);
buffer.byteLength;
// 32

如果要丰碑的内存区域很大, 是有可能分配失败的(没有那么多的连续空余内存), 所以有必要检查是否分配成功.

if (buffer.byteLength === n) {
// 成功
} else {
// 失败
}

4.2 ArrayBuffer.prototype.slice()

ArrayBuffer实例有一个slice方法, 允许将内存区域的一部分, 拷贝生成一个新的ArrayBuffer对象.

const buffer = new ArrayBuffer(8);
const newBuffer = buffer.slice(0, 3);

除了slice方法, ArrayBuffer不提供任何直接读写内存的方法, 只允许在其上方建立视图, 然后通过视图读写.

4.3 ArrayBuffer.isView()

isView返回一个布尔值, 表示参数是否为ArrayBuffer的视图实例, 这个方法大致相当于判断参数是否为TypedArray实例或DataView实例:

const buffer = new ArrayBuffer(8);
ArrayBuffer.isView(buffer); // false

const v = new Int32Array(buffer);
ArrayBuffer.isView(v); // true

5. ArrayBuffer <--> string

主要使用的是原生的TextEncoderTextDecoder方法

/**
* Convert ArrayBuffer/TypedArray to String via TextDecoder
*
* @see https://developer.mozilla.org/en-US/docs/Web/API/TextDecoder
*/
function ab2str(
input: ArrayBuffer | Uint8Array | Int8Array | Uint16Array | Int16Array | Uint32Array | Int32Array,
outputEncoding: string = 'utf8'
): string {
const decoder = new TextDecoder(outputEncoding);
return decoder.decode(input);
}

/**
* Convert String to ArrayBuffer via TextEncoder
*
* @see https://developer.mozilla.org/zh-CN/docs/Web/API/TextEncoder
*/
function str2ab(input: string): ArrayBuffer {
const view = str2Uint8Array(input);
return view.buffer;
}

/** Convert String to Uint8Array */
function str2Uint8Array(input: string): Uint8Array {
const encoder = new TextEncoder();
const view = encoder.encode(input);
return view;
}

6. TypedArray 视图

ArrayBuffer作为内存区域, 可以存放多种类型的数据, 同一段内存, 不同数据有不同的解毒方式, 这就叫做视图. ArrayBuffer有两种视图, 一种是TypedArray视图, 另一种是DataView视图. 前者的数组成员都是同一个数据类型, 后者的数组成员可以使不同的数据类型.

TypedArray有 9 种类型, 每一种视图都是一种构造函数:

  • Int8Array: 8 位有符号整数,长度 1 个字节。
  • Uint8Array: 8 位无符号整数,长度 1 个字节。
  • Uint8ClampedArray: 8 位无符号整数,长度 1 个字节,溢出处理不同。
  • Int16Array: 16 位有符号整数,长度 2 个字节。
  • Uint16Array: 16 位无符号整数,长度 2 个字节。
  • Int32Array: 32 位有符号整数,长度 4 个字节。
  • Uint32Array: 32 位无符号整数,长度 4 个字节。
  • Float32Array: 32 位浮点数,长度 4 个字节。
  • Float64Array: 64 位浮点数,长度 8 个字节。

TypeArray视图上所有的数组方法在它上面都能使用. 普通数组与 TypedArray 数组的差异主要有以下几点:

  • TypedArray 数组的所有成员都是同一类型
  • TypedArray 数组的成员是连续的, 不会有空位
  • TypedArray 数组的成员默认值为 0.
  • TypedArray 本质只是一个视图, 本身不存储数据, 数据都存储在底层的ArrayBuffer中.

6.1 构造 TypedArray

构造TypedArray有 9 中构造函数, 用来生成响应类型的数组实例.

构造函数有四种用法(?:可选),=:默认:

  1. TypedArray(buffer, bytedOffset?=0, length?): ArrayBuffer 对象, 字节序号, 数据个数
  2. TypedArray(length): 直接分配内存生成
  3. TypedArray(typedArray): 接受另一个TypedArray实例作为参数. 此时对应的此等内存是不一样的, 会开辟一段新的内存存储数据, 不会再原数组的内存之上建立视图.
  4. TypedArray(arrayLikeObject): 接受一个普通数组, 然后直接生成TypedArray实例.

当然 TypedArray 数组也可以转换回普通数组.

const normalArray = [...typedArray];
// or
const normalArray = Array.from(typedArray);
// or
const normalArray = Array.prototype.slice.call(typedArray);

几乎所有的数组方法都可以在TypedArray上使用, 但是TYpedArray没有concat方法, 如果想要合并多个TypedArray数组, 可以使用下面这个方法:

function concatenate(resultConstructor, ...arrays) {
let totalLength = 0;
for (let arr of arrays) {
totalLength += arr.length;
}
let result = new resultConstructor(totalLength);
let offset = 0;
for (let arr of arrays) {
result.set(arr, offset);
offset += arr.length;
}
return result;
}

concatenate(Uint8Array, Uint8Array.of(1, 2), Uint8Array.of(3, 4));
// Uint8Array [1, 2, 3, 4]

此外, TypedArray数组与普通数组一样, 部署了Iterator接口, 所以可以被遍历.

let ui8 = Uint8Array.of(0, 1, 2);
for (let byte of ui8) {
console.log(byte);
}
// 0
// 1
// 2

6.2TypedArray 方法

6.2.1 TypedArray.prototype.buffer

该方法返回整段内存区域对应的ArrayBuffer对象, 归属性为只读属性:

const a = new Float32Array(64);
const b = new Uint8Array(a.buffer);

6.2.2 TypedArray.prototype.byteLength,TypedArray.prototype.byteOffset

byteLength属性返回TypedArray数组占据的内存长度, 单位为字节. byteOffset属性返回TypedArray数组从底层ArrayBuffer对象的哪个字节开始, 这两个属性都只是只读属性.

const b = new ArrayBuffer(8);

const v1 = new Int32Array(b);
const v2 = new Uint8Array(b, 2);
const v3 = new Int16Array(b, 2, 2);

v1.byteLength; // 8
v2.byteLength; // 6
v3.byteLength; // 4

v1.byteOffset; // 0
v2.byteOffset; // 2
v3.byteOffset; // 2

6.2.3 TypedArray.prototype.length

该属性表示TypedArray数组含有多少个成员, 注意将length属性和byteLength属性区分, 前者是成员长度, 后者是字节长度.

const a = new Int16Array(8);

a.length; // 8
a.byteLength; // 16

6.2.4 TypedArray.prototype.set()

set方法用于复制数据, 将一段内容完全复制到另一段内存:

const a = new Uint8Array(8);
const b = new Uint8Array(8);

b.set(a);

上面代码复制 a 数组的内容到 b 数组,它是整段内存的复制,比一个个拷贝成员的那种复制快得多。

set 方法还可以接受第二个参数,表示从 b 对象的哪一个成员开始复制 a 对象。

const a = new Uint16Array(8);
const b = new Uint16Array(10);

b.set(a, 2);

6.2.5 TypedArray.prototype.subarray()

subarray可以针对TypedArray数组的一部分, 再建立一个新的视图:

const a = new Uint16Array(8);
const b = a.subarray(2, 3);

a.byteLength; // 16
b.byteLength; // 2

6.2.6 TypedArray.prototype.slice()

slice方法,可以返回一个指定位置的新的 TypedArray 实例。

let ui8 = Uint8Array.of(0, 1, 2);
ui8.slice(-1);
// Uint8Array [ 2 ]

6.2.7 TypedArray.of()

用于将参数转为一个 TypedArray 实例:

Float32Array.of(0.151, -8, 3.7);
// Float32Array [ 0.151, -8, 3.7 ]

6.2.8 TypedArray.from()

Uint16Array.from([0, 1, 2]);
// Uint16Array [ 0, 1, 2 ]

7. 复合视图

由于视图的构造函数可以指定起始位置和长度, 所以在同一段内存之中, 可以依次存放不同的数据, 就叫做"复合视图":

const buffer = new ArrayBuffer(24);

const idView = new Uint32Array(buffer, 0, 1);
const usernameView = new Uint8Array(buffer, 4, 16);
const amountDueView = new Float32Array(buffer, 20, 1);

这种数据结构用 c 语言来描述就是:

struct someStruct {
unsigned long id;
char username[16];
float amountDue;
};

8. DataView 视图

除了上面那种复合视图, ArrayBuffer实际上提供了DataView来创建复合视图, 并提供了一些操作方法:

DataView支持设定字节序. 设计目的在于处理网络设备传来的数据.

DataView本身也是构造函数, 接受一个ArrayBuffer对象作为参数生成视图:

DataView(ArrayBuffer buffer [, 字节起始位置 [, 长度]]);

DataView实例具有以下属性, 含义与TypedArray相同.

  • DataView.prototype.buffer:返回对应的 ArrayBuffer 对象
  • DataView.prototype.byteLength:返回占据的内存字节长度
  • DataView.prototype.byteOffset:返回当前视图从对应的 ArrayBuffer 对象的哪个字节开始

DataView实例提供 8 个方法读取内存:

  • getInt8:读取 1 个字节,返回一个 8 位整数。
  • getUint8:读取 1 个字节,返回一个无符号的 8 位整数。
  • getInt16:读取 2 个字节,返回一个 16 位整数。
  • getUint16:读取 2 个字节,返回一个无符号的 16 位整数。
  • getInt32:读取 4 个字节,返回一个 32 位整数。
  • getUint32:读取 4 个字节,返回一个无符号的 32 位整数。
  • getFloat32:读取 4 个字节,返回一个 32 位浮点数。
  • getFloat64:读取 8 个字节,返回一个 64 位浮点数

这一系列get方法的参数都是一个字节序号, 表示从哪个字节开始读取.

const buffer = new ArrayBuffer(24);
const dv = new DataView(buffer);

// 从第1个字节读取一个8位无符号整数
const v1 = dv.getUint8(0);

// 从第2个字节读取一个16位无符号整数
const v2 = dv.getUint16(1);

// 从第4个字节读取一个16位无符号整数
const v3 = dv.getUint16(3);

如果一次读取两个及以上字节, 几必须明确数据的存储方式, 到底是小端字节序还是大端字节序. 默认下, DataView是的get方法是使用大端字节序解读数据, 如果需要使用小端字节序解读, 必须在get方法的第二个参数指定true:

// 小端字节序
const v1 = dv.getUint16(1, true);

// 大端字节序
const v2 = dv.getUint16(3, false);

// 大端字节序
const v3 = dv.getUint16(3);

DataView视图提供 8 个方法写入内存:

  • setInt8:写入 1 个字节的 8 位整数。
  • setUint8:写入 1 个字节的 8 位无符号整数。
  • setInt16:写入 2 个字节的 16 位整数。
  • setUint16:写入 2 个字节的 16 位无符号整数。
  • setInt32:写入 4 个字节的 32 位整数。
  • setUint32:写入 4 个字节的 32 位无符号整数。
  • setFloat32:写入 4 个字节的 32 位浮点数。
  • setFloat64:写入 8 个字节的 64 位浮点数。

这一系列set方法类似get, 接受两个参数, 第一个参数是字节序列, 表示从哪个字节开始写入, 第二个参数为写入的数据. 对于那些写入两个以上字节的方法, 需要在第三个参数指定, falseundefined表示大端字节序, true表示小端字节序.

// 在第1个字节,以大端字节序写入值为25的32位整数
dv.setInt32(0, 25, false);

// 在第5个字节,以大端字节序写入值为25的32位整数
dv.setInt32(4, 25);

// 在第9个字节,以小端字节序写入值为2.5的32位浮点数
dv.setFloat32(8, 2.5, true);

不确定字节序的话, 可以使用这个方法进行判断:

const littleEndian = (function() {
const buffer = new ArrayBuffer(2);
new DataView(buffer).setInt16(0, 256, true);
return new Int16Array(buffer)[0] === 256;
})();

返回true为小端字节序, 返回false为大端字节序.

9. ArrayBuffer 的应用

9.1 Ajax

XMLHttpRequest第二版XHR2允许服务器返回二进制数据, 如果明确知道返回二进制数据类型, 可以把返回类型responseType设置为arraybuffer, 如果不知道就设为blob

let xhr = new XMLHttpRequest();
xhr.open('GET', someUrl);
xhr.responseType = 'arraybuffer';

xhr.onload = function() {
let arrayBuffer = xhr.response;
// ···
};

xhr.send();

如果知道传回来的是 32 位整数, 可以向这样处理:

xhr.onreadystatechange = function() {
if (req.readyState === 4) {
const arrayResponse = xhr.response;
const dataView = new DataView(arrayResponse);
const ints = new Uint32Array(dataView.byteLength / 4);

xhrDiv.style.backgroundColor = '#00FF00';
xhrDiv.innerText = 'Array is ' + ints.length + 'uints long';
}
};

9.2 Canvas

canvas元素输出的二进制像素数据, 就是typedArray数组.

const canvas = document.getElementById('myCanvas');
const ctx = canvas.getContext('2d');

const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
const uint8ClampedArray = imageData.data;

uint8ClampedArray是一种针对canvsa转悠的类型, 特点是针对颜色, 把每个字节解读为无符号的 8 位整数, 在发生运算的时候自动过滤高位移除, 为图像处理带来了巨大的方便.

比如, 如果把像素的颜色值设置Uint8Array类型, 那么乘以一个 gamma, 就必须这样计算:

u8[i] = Math.min(255, Math.max(0, u8[i] * gamma));

而只用Uint8ClampArray就只需要:

pixels[i] *= gamma;

9.3 WebSocket

websocket可以通过arraybuffer发送或者接受二进制数据:

let socket = new WebSocket('ws://127.0.0.1:8081');
socket.binaryType = 'arraybuffer';

// Wait until socket is open
socket.addEventListener('open', function(event) {
// Send binary data
const typedArray = new Uint8Array(4);
socket.send(typedArray.buffer);
});

// Receive binary data
socket.addEventListener('message', function(event) {
const arrayBuffer = event.data;
// ···
});

9.4 Fetch Api

FetchApi 取回的数据就是ArrayBuffer对象:

fetch(url)
.then(function(response) {
return response.arrayBuffer();
})
.then(function(arrayBuffer) {
// ...
});

9.5 File Api

如果知道一个文件的二进制数据类型, 也可以将这个文件读取为ArrayBuffer对象.

const fileInput = document.getElementById('fileInput');
const file = fileInput.files[0];
const reader = new FileReader();
reader.readAsArrayBuffer(file);
reader.onload = function() {
const arrayBuffer = reader.result;
// ···
};

这是一个读取 BMP 文件的示例, 假定 file 变量是一个指向 bmp 文件的文件对象:

const reader = new FileReader();
reader.addEventListener('load', processimage, false);
reader.readAsArrayBuffer(file);

然后定义处理图像的回调函数: 简历DataView视图, 在建立一个bitmap对象用于存放处理后的数据, 最后将图像展示在canvas元素之中:

function processimage(e) {
const buffer = e.target.result;
const datav = new DataView(buffer);
const bitmap = {};
// 具体的处理步骤
}

具体处理图像数据, 先处理 bmp 的文件头:

bitmap.fileheader = {};
bitmap.fileheader.bfType = datav.getUint16(0, true);
bitmap.fileheader.bfSize = datav.getUint32(2, true);
bitmap.fileheader.bfReserved1 = datav.getUint16(6, true);
bitmap.fileheader.bfReserved2 = datav.getUint16(8, true);
bitmap.fileheader.bfOffBits = datav.getUint32(10, true);

处理图像图元信息:

bitmap.infoheader = {};
bitmap.infoheader.biSize = datav.getUint32(14, true);
bitmap.infoheader.biWidth = datav.getUint32(18, true);
bitmap.infoheader.biHeight = datav.getUint32(22, true);
bitmap.infoheader.biPlanes = datav.getUint16(26, true);
bitmap.infoheader.biBitCount = datav.getUint16(28, true);
bitmap.infoheader.biCompression = datav.getUint32(30, true);
bitmap.infoheader.biSizeImage = datav.getUint32(34, true);
bitmap.infoheader.biXPelsPerMeter = datav.getUint32(38, true);
bitmap.infoheader.biYPelsPerMeter = datav.getUint32(42, true);
bitmap.infoheader.biClrUsed = datav.getUint32(46, true);
bitmap.infoheader.biClrImportant = datav.getUint32(50, true);

最后处理图像本身的像素信息:

const start = bitmap.fileheader.bfOffBits;
bitmap.pixels = new Uint8Array(buffer, start);

然后下一步就可以根据需要进行图像变形或者转换格式.

9.6 SharedArrayBuffer

浏览器引入了web worker作为多线程的实现.

不同的worder线程之间可以通过postMessage进行通信.

// 主线程
const w = new Worker('myworker.js');

// 主线程
w.postMessage('hi');
w.onmessage = function(ev) {
console.log(ev.data);
};

// Worker 线程
onmessage = function(ev) {
console.log(ev.data);
postMessage('ho');
};

线程之间的数据交换可以是二进制的. 这种交换采用的是复制机制, 在数据量较大的时候会比较低效, 此时使用SharedArrayBuffer(ES7), 可以运行 worker 共享同一块内存. 达到数据分享的目的. SharedArrayBufferArrayBuffer一样, 唯一的区别在于后者无法共享数据.

// 主线程

// 新建 1KB 共享内存
const sharedBuffer = new SharedArrayBuffer(1024);

// 主线程将共享内存的地址发送出去
w.postMessage(sharedBuffer);

// 在共享内存上建立视图,供写入数据
const sharedArray = new Int32Array(sharedBuffer);

// Worker 线程
onmessage = function(ev) {
// 主线程共享的数据,就是 1KB 的共享内存
const sharedBuffer = ev.data;

// 在共享内存上建立视图,方便读写
const sharedArray = new Int32Array(sharedBuffer);

// ...
};

9.7. Atomic 对象

多线程共享内存, 就会出现如何防止两个线程同时修改某个地址的问题. SharedArrayBufferAPI 提供Atomics对象, 保证所有共享内存的操作都是"原子性"的, 并且可以在所有线程内同步.

9.7.1 Atomics.store(),Atomics.load()

store()方法用来向共享内存写入数据, load()方法用来从共享内存读取数据.

Atomics.load(array, index);
Atomics.store(array, index, value);

store方法接受三个参数: SharedBuffer的视图, 位置索引和值, 返回sharedArray[index]的值.

load方法接受两个参数: SharedBuffer的视图和位置索引, 也是返回sharedArray[index]的值.

// 主线程
const worker = new Worker('worker.js');
const length = 10;
const size = Int32Array.BYTES_PER_ELEMENT * length;
// 新建一段共享内存
const sharedBuffer = new SharedArrayBuffer(size);
const sharedArray = new Int32Array(sharedBuffer);
for (let i = 0; i < 10; i++) {
// 向共享内存写入 10 个整数
Atomics.store(sharedArray, i, 0);
}
worker.postMessage(sharedBuffer);

// worker.js
self.addEventListener(
'message',
event => {
const sharedArray = new Int32Array(event.data);
for (let i = 0; i < 10; i++) {
const arrayValue = Atomics.load(sharedArray, i);
console.log(`The item at array index ${i} is ${arrayValue}`);
}
},
false
);

9.7.2 Atomics.exchange()

exchange方法是除了store外的另一种写入数据的方法, 区别在于store返回写入的值, exchange返回被替换的值.

// Worker 线程
self.addEventListener(
'message',
event => {
const sharedArray = new Int32Array(event.data);
for (let i = 0; i < 10; i++) {
if (i % 2 === 0) {
const storedValue = Atomics.store(sharedArray, i, 1);
console.log(`The item at array index ${i} is now ${storedValue}`);
} else {
const exchangedValue = Atomics.exchange(sharedArray, i, 2);
console.log(`The item at array index ${i} was ${exchangedValue}, now 2`);
}
}
},
false
);

上面的代码将共享内存的偶数位置的值改为 1, 奇数位置的值改为 2

9.7.3 Atomics.wait(),Atomics.wake()

wait方法和wake方法用于等待通知. 这两个方法相当于锁内存, 记载一个县城进行操作时, 让其他线程休眠, 等到操作结束, 才唤醒那些休眠的线程:

// Worker 线程
self.addEventListener(
'message',
event => {
const sharedArray = new Int32Array(event.data);
const arrayIndex = 0;
const expectedStoredValue = 50;
Atomics.wait(sharedArray, arrayIndex, expectedStoredValue);
console.log(Atomics.load(sharedArray, arrayIndex));
},
false
);

wait方法告诉 worder 只要满足给定条件, 就在这一行 worder 线程进入休眠.

主线程一旦更改了指定位置的值, 就可以唤醒 Worker 线程:

// 主线程
const newArrayValue = 100;
Atomics.store(sharedArray, 0, newArrayValue);
const arrayIndex = 0;
const queuePos = 1;
Atomics.wake(sharedArray, arrayIndex, queuePos);

Atomics.wait()方法的使用格式如下:

Atomics.wait(sharedArray, index, value, timeout);

分别为: 共享内存的视图数组, 视图数据的位置, 该位置的预期值, 经过多少毫秒后自动唤醒(默认无穷大)

返回三种字符串:

  • 如果sharedArray[index]不等于value, 返回not-equal, 否则进入休眠
  • 如果wake唤醒, 就返回ok
  • 如果超时唤醒, 返回timed-out

Atomice.wake()方法的使用格式如下:

Atomics.wake(sharedArray, index, count);

分别为: 共享内存的视图数组, 视图数据的位置, 需要唤醒的 worker 数量(默认无穷大)

wake方法一旦唤醒休眠的 worder 线程, 就会让它继续往下运行.

// 主线程
console.log(ia[37]); // 163
Atomics.store(ia, 37, 123456);
Atomics.wake(ia, 37, 1);

// Worker 线程
Atomics.wait(ia, 37, 163);
console.log(ia[37]); // 123456

9.7.4 运算方法: add, sub, and, or, xor

  • Atomics.add(sharedArray, index, value): 将value加到sharedAttay[index]
  • Atomics.sub(sharedArray, index, value): 将valuesharedAttay[index]减去
  • Atomics.and(sharedArray, index, value): 将valuesharedAttay[index]进行and位运算
  • Atomics.or(sharedArray, index, value): 将valuesharedAttay[index]进行or位运算
  • Atomics.xor(sharedArray, index, value): 将valuesharedAttay[index]进行xor位运算

9.7.5 其他方法

  • Atomics.compareExchange(sharedArray, index, oldval, newval): 如果sharedArray[index]等于oldval,就写入newval,返回oldval, 可以用于从buffer读取一个值, 单后对该值进行操作, 操作完检查值是否发生变化.
  • Atomics.isLockFree(size): 返回一个布尔值,表示Atomics对象是否可以处理某个size的内存锁定。如果返回false,应用程序就需要自己来实现锁定。