前回からのつづき

前回、共有メモリを使用したデータの受け渡しを中心に説明してきました。

共有メモリを使用したアクセスでは、同時アクセスが可能である為、メインスレッドと 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 アプリケーション開発で利用する機会はほとんどありません。

(つづく)