ES2024系列 - Atomics.waitAsync()

331 阅读5分钟

介绍

Atomics.waitAsync对应的方法是Atomics.wait,它们都是在等待SharedArrayBuffer所代表的共享内存的某个位置的值变化。 不同的是,Atomics.wait是让线程睡眠来等待,从而致使线程阻塞,这种阻塞在UI线程是不可接受的,它会导致整个页面卡死无法响应,因此如果你在UI主线程调用Atomics.wait,浏览器会抛出异常。

Atomics.waitAsync可以让线程异步的等待,这样线程不会阻塞,因此它可以用在UI主线程中,也可以用在其它线程中。按照JavaScript的习惯,它应该会返回Promise,不过这里情况有点特殊,Promise是被包在一个对象中返回的,而且有时候不会返回Promise。 请往下看。

我们先举个例子来看一下如何使用它:

UI主线程:

  // 创建Web Worker
  const worker = new Worker('worker.js');

  // 创建SharedArrayBuffer
  const length = 10;
  const sharedBuffer = new SharedArrayBuffer(length * Int32Array.BYTES_PER_ELEMENT);

  // 用Int32Array初始化sharedBuffer
  const sharedArray = new Int32Array(sharedBuffer); 
  for (let i = 0; i < length; i++)  {
    sharedArray[i] = 0;
  }

  // 将sharedBuffer传给Web Worker,采用structuredClone深度拷贝
  worker.postMessage(sharedBuffer);
  // 注意,它返回的是一个对象,async是true时,value才是Promise
  const {async, value} = Atomics.waitAsync(sharedArray, 0, 0);
  (async function() {
	if(async) {
		console.log('async', async, 'value', await value);
	} else {
		// 初始值不匹配或者timeout时间是0
		console.log('没有等待的原因', value);
	}
  })();

worker.js文件:

self.addEventListener('message', m => {
  // m.data就是sharedBuffer的拷贝
  // 基于sharedBuffer创建Int32Array
  const sharedArray = new Int32Array(m.data);

  // 模拟从服务端获取数据
  setTimeout(function() {
    console.log('开始传递数据', Date.now());
    // 最关键的一步,将数据放入共享内存
	Atomics.store(sharedArray, 0, 100);
    Atomics.notify(sharedArray, 0);
  }, 5000);
});

从例子中可以看到,UI主线程中调用了Atomics.waitAsync,来等待Worker线程的Atomics.notify,整个UI线程不会被阻塞。

我们来看一下它的具体用法。

具体用法与含义

Atomics.waitAsync的意思是当指定位置的值与期望值一致时,就继续等待。

语法

Atomics.waitAsync(typedArray, index, value)
Atomics.waitAsync(typedArray, index, value, timeout)

参数

参数:TypedArray

必须是一个基于SharedArrayBufferInt32Array或者BigInt64Array类型。

参数:index

TypedArray中的索引

参数:value

index索引处的期望值,当实际值和期望值一致时,Atomics.waitAsync将继续等待

参数:timeout(可选)

毫秒值,等待超过该时间就不再继续等待了。

以下情况会被认为没有超时时间:

  1. 非数字值。
  2. 不传该参数。

负值会被转成00会被认为不能等待。

返回值

返回值是一个Object对象。它有两个属性:

async属性:

true或者false,布尔值,用于告知当前函数是异步还是同步,异步就是true,同步是false。异步时,value属性是一个Promise,同步时value属性是一个字符串。

value属性

asynctrue时,也就是说函数是异步的,这时value属性就是PromisePromiseresolve时,其值可能是"ok"或者"timed-out""ok"代表正常的等待并且值已经改变。"time-out"代表等待超过了timeout参数设定的时间。该Promise没有被reject的情况。

asyncfalse时,也就是说函数是同步的,这时value就是一个字符串,其值有可能是"not-equal"或者"time-out""not-equal"说明value期望值参数和实际值不符,"time-out"说明timeout参数是0

返回值综述

返回值只有这三种情况: { async: false, value: "not-equal" } { async: false, value: "timed-out" } { async: true, value: promise }

这个API有时会是同步的,有时会是异步的。

同步的情况: 1. 期望值和实际值不同,这时会立即返回,没有等待的必要。 2. timeout参数是0,立即超时返回。

异步的情况: 除了同步的情况外,其他情况下都是异步的。

抛异常的情况

TypedArray参数如果不是基于SharedArrayBufferInt32Array或者BigInt64Array类型,则会抛TypeError类型异常。

index参数超过了TypedArray的范围时抛RangeError类型异常。

各种情况说明

Atomics.waitAsync的使用限制较少。

我们还延续上面的例子,将主线程的代码修改如下,将worker.js的代码都删掉:

const worker = new Worker('worker.js');

const length = 10;
const sharedBuffer = new SharedArrayBuffer(length * Int32Array.BYTES_PER_ELEMENT); 

const sharedArray = new Int32Array(sharedBuffer); 
for (let i = 0; i < length; i++)  {
	sharedArray[i] = 0;
}
worker.postMessage(sharedBuffer);

// 注意看下面的代码 
const {async: async1, value: value1} = Atomics.waitAsync(sharedArray, 0, 0);
const {async: async2, value: value2} = Atomics.waitAsync(sharedArray, 0, 0);
  
    
if(async2) {
	value2.then((returnedValue) => {
		console.log('async2', async2, 'returnedValue2', returnedValue);
	});
} else {
	console.log('没有等待的原因', value2);
}

if(async1) {
	value1.then((returnedValue) => {
		console.log('async1', async1, 'returnedValue1', returnedValue);
	});
} else {
	console.log('没有等待的原因', value1);
}
Atomics.notify(sharedArray, 0);

// 控制台打印
// async1 true returnedValue1 ok
// async2 true returnedValue2 ok

可以看到,我们在同一个内存地址wait了两次,这是可以的,而且还可以在不同的WorkerwaitPromiseresolve的先后顺序按照先入先出的原则,谁先wait谁先被notify

另外可以在同一个线程对同一个SharedArrayBuffer的同一个位置进行waitnotify,也就是说线程可以notify自己。

Stage阶段和浏览器实现

目前除了Firefox,其他主流浏览器均已实现该特性。

但是最积极的Chrome是于2020年实现的该特性,考虑到国内的浏览器更新速度,目前还不建议大量使用该特性,除非你的客户端版本可控。

Polyfill

目前还没有可以完全模拟的Polyfill。

标准提议仓库中提供了一个Polyfill,通过另开一个Worker来实现异步,Worker比较重量级的,因此该 Polyfill 性能不是很好。 要查看该 Polyfill 可以点击此处

结束语

Atomics.waitAsync更加完善了Web Worker,CPU密集型的应用在 Web 前端的可能性变得越来越大。