前回からのつづき
前回、共有メモリを使用したデータの受け渡しを中心に説明してきました。
共有メモリを使用したアクセスでは、同時アクセスが可能である為、メインスレッドと Web Worker との間で競合が発生する可能性があり、それを防止する仕組みとして Atomics API が用意されている説明をしました。
今回は、 Atomics API について、もう少し具体的に説明を行っていきたいと思います。
前回のおさらい
よく使用する Atomics API の一覧を示します。
- Atomics.add
- Atomics.load
- Atomics.store
- Atomics.sub
- Atomics.and
- Atomics.or
- Atomics.xor
- Atomics.exchange
- Atomics.compareExchange
- Atomics.wait
- Atomics.notify
- Atomics.waitAsync(対応ブラウザ要確認)
- Atomics.isLockFree
以降、各 API について詳しくみていきます。
Atomics.add
共有メモリ領域( SharedArrayBuffer )の指定された位置への不可分な加算機能を提供します。
Atomics.add(第1引数, 第2引数, 第3引数);
■ パラメータ
- 第1引数:TypedArray オブジェクト
- 第2引数:TypedArray オブジェクト内の配列の指標位置
- 第3引数:加算値
■ 戻り値
加算前の値
例
const sab = new SharedArrayBuffer(4);
const view = new Int32Array(sab);
Atomics.add(view, 0, 5);
結果
0
↓
5
もう一回、
const old = Atomics.add(view, 0, 10);
console.log(view[0]);
console.log(old);
結果
15
5
Atomics.load
共有メモリ領域( SharedArrayBuffer )の指定された位置への不可分な読込機能を提供します。
Atomics.load(第1引数, 第2引数);
■ パラメータ
- 第1引数:TypedArray オブジェクト
- 第2引数:TypedArray オブジェクト内の配列の指標位置
■ 戻り値
現在の値
例:
const sab = new SharedArrayBuffer(4);
const view = new Int32Array(sab);
const value = Atomics.load(view, 0);
指定した位置の値を、途中で破損した状態にならないよう、不可分に読み込みます。
Atomics.store
共有メモリ領域( SharedArrayBuffer )の指定された位置への不可分な書き込み機能を提供します。
Atomics.store(第1引数, 第2引数, 第3引数);
■ パラメータ
- 第1引数:TypedArray オブジェクト
- 第2引数:TypedArray オブジェクト内の配列の指標位置
- 第3引数:書き込む値
■ 戻り値
書き込んだ値
例:
const sab = new SharedArrayBuffer(4);
const view = new Int32Array(sab);
Atomics.store(view, 0, 100);
Atomics.sub
共有メモリ領域( SharedArrayBuffer )の指定された位置への不可分な減算機能を提供します。
Atomics.sub(第1引数, 第2引数, 第3引数);
■ パラメータ
- 第1引数:TypedArray オブジェクト
- 第2引数:TypedArray オブジェクト内の配列の指標位置
- 第3引数:減算値
■ 戻り値
減算前の値
例
const sab = new SharedArrayBuffer(4);
const view = new Int32Array(sab);
Atomics.sub(view, 0, 1);
結果
0
↓
-1
Atomics.and / Atomics.or / Atomics.xor
共有メモリ領域( SharedArrayBuffer )の指定された位置への不可分なビット演算機能を提供します。
Atomics.and(第1引数, 第2引数, 第3引数);
Atomics.or(第1引数, 第2引数, 第3引数);
Atomics.xor(第1引数, 第2引数, 第3引数);
■ パラメータ
- 第1引数:TypedArray オブジェクト
- 第2引数:TypedArray オブジェクト内の配列の指標位置
- 第3引数:ビット値
■ 戻り値
ビット演算前の値
const sab = new SharedArrayBuffer(4);
const view = new Uint8Array(sab);
Atomics.and(view, 0, 0b11101110); // AND 演算
Atomics.or(view, 0, 0b11101110); // OR 演算
Atomics.xor(view, 0, 0b11101110); // XOR 演算
Atomics.exchange
共有メモリ領域( SharedArrayBuffer )の指定された位置の不可分な値の交換機能を提供します。
Atomics.exchange(第1引数, 第2引数, 第3引数);
■ パラメータ
- 第1引数:TypedArray オブジェクト
- 第2引数:TypedArray オブジェクト内の配列の指標位置
- 第3引数:交換する値
■ 戻り値
交換前の値
const sab = new SharedArrayBuffer(4);
const view = new Int32Array(sab);
Atomics.exchange(view, 0, 50);
Atomics.compareExchange
共有メモリ領域( SharedArrayBuffer )の指定された位置の値が比較する値と同じなら、不可分な値の交換機能を提供します。比較する値と頃なる場合は、交換を行いません。
Atomics.compareExchange(第1引数, 第2引数, 第3引数, 第4引数);
■ パラメータ
- 第1引数:TypedArray オブジェクト
- 第2引数:TypedArray オブジェクト内の配列の指標位置
- 第3引数:比較する値
- 第4引数:交換する値
■ 戻り値
交換前の値
例えば、
Atomics.compareExchange(
view,
0,
100,
200
);
これは、もし 100 なら 200 へ変更し、違えば変更しないという意味になります。
👉 CAS (Compare And Swap)と呼ばれ、ロック機能を作る時によく使われます。
Atomics.wait
共有メモリ領域(SharedArrayBuffer)の指定された位置の値が期待値と同じであれば待機し、異なる場合は待機せず "not-equal" を返します。
最大待機時間が指定されている場合は、その時間をこえると再開され、"timed-out"を返します。
Atomics.wait(第1引数, 第2引数, 第3引数, 第4引数);
■ パラメータ
- 第1引数:TypedArray オブジェクト
- 第2引数:TypedArray オブジェクト内の配列の指標位置
- 第3引数:期待値
- 第4引数:最大待機時間(ミリ秒)
■ 戻り値
- "ok" … notify によって起こされた
- "not-equal" … 最初から値が期待値と違っていた
- "timed-out" … タイムアウトした
この機能を利用することにより、Web Worker の CPU を消費せずに、Web Worker の実行を現在の場所で待機させます。この待機は、イベント待ちとは異なり、イベントループそのものを停止します。
Atomics.wait(view, 0, 0);
の場合、view[0] の値が 0 なら、その時点で待機状態になります。待機中のスレッドは、Atomics.notify() によって再開されるか、最大待機時間を超えると "timed-out" を返して再開されます。その為、再開されるまで、他のイベントに対する処理も行われなくなります。
※ Atomics.wait() は非常に強力な API ですが、多くの Web アプリケーションでは postMessage() を利用したメッセージ通信で十分です。Atomics.wait() が真価を発揮するのは、SharedArrayBuffer を利用した高度な並列処理や、そのような機能を提供するライブラリの内部実装です。
Atomics.notify
共有メモリ領域(SharedArrayBuffer)の指定された位置で待機中のスレッドに対して、再開を通知します。
Atomics.notify(第1引数, 第2引数);
■ パラメータ
- 第1引数:TypedArray オブジェクト
- 第2引数:TypedArray オブジェクト内の配列の指標位置
■ 戻り値
起こしたスレッド数
Web Worker メインスレッド
┌───────────────────────────────┐ ┌───────────────────────────────┐
│ │ │ │
│ Atomics.wait(view, 0, 0); │ │ │
│ │ │ │
| ────────── WAIT ────────── ◀───────────── Atomics.notify(view, 0); │
│ │ │ │ │
│ ▼ │ │ │
| console.log("Wake Up"); │ │ │
│ │ │ │
└───────────────────────────────┘ └───────────────────────────────┘
になります。
Atomics.notify(view, 0); は、指定位置で待機しているスレッドに再開を通知します。
Atomics.waitAsync
Atomics.wait と同様、共有メモリ領域( SharedArrayBuffer )の指定された位置の値が期待値と同じであれば待機します。
const result = Atomics.waitAsync(view, 0, 0);
if (result.async) {
console.log(await result.value);
} else {
console.log(result.value);
}
■ パラメータ
- 第1引数:TypedArray オブジェクト
- 第2引数:TypedArray オブジェクト内の配列の指標位置
- 第3引数:期待値
- 第4引数:最大待機時間(ミリ秒)
■ 戻り値
待機が発生しない場合
{
async: false,
value: "not-equal" or "timed-out"(最大待機時間0m秒の場合)
}
待機が発生する場合
{
async: true,
value: Promise
}
await value の結果は
- "ok" … notify によって起こされた
- "timed-out" … タイムアウトした
この機能が、Atomics.wait と異なるのは、その待機の違いです。Atomics.wait がイベントループそのものを停止してしまうのに比べ、戻り値が、 Promise オブジェクトであることからも分かるように、本機能は、イベントループを止めずに、待機することが可能です。
つまり、待機中も UI をはじめ、他のイベントの処理が可能となります。
ただし、対応ブラウザはまだ確認が必要なので、実際に利用する場合は対象ブラウザのサポート状況を確認してください。
Atomics.isLockFree
指定したサイズ(バイト数)の Atomic 操作を、現在の CPU がロックなし(Lock-Free)で実行できるかどうかを調べます。
Atomics.isLockFree(第1引数);
■ パラメータ
- 第1引数:バイト数
■ 戻り値
true / false
例えば、
Atomics.add(view, 0, 1);
を実行する場合に、CPU が「4バイト整数を一発で加算できる命令」を持っていれば CPU 命令1個で済みます。
この場合、この処理を実行する際、他の命令が割り込まないようにロックをかける必要がありません。
この状態を、Lock-Free と呼びばす。
ところが、持っていない場合、CPU が単一命令で実行できないため、内部では複数の命令やロックを用いて実現される場合があります。
ロックする
読む
計算
書く
ロック解除
この状態を Lock-based と呼びます。
Atomics.isLockFree(4)
は、4バイトの Atomic 操作を、現在の CPU が Lock-Free で実行できるかどうかを判定して返します。
※ Atomics.isLockFree() は、指定したサイズの Atomic 操作を、現在の環境でロックを使わず実行できるかを判定するための API です。主にライブラリやランタイムの実装者向けで、通常の Web アプリケーション開発で利用する機会はほとんどありません。
(つづく)