Atomicsで共有メモリ上のデータを安全に取り扱う
AtomicsはSharedArrayBufferなどで定義された共有メモリ上のデータを安全に操作するための仕組みで、不可分操作や待機・通知による制御を提供します。新たに追加されたAtomics.pauseにより、スピンロック中のリソース最適化も可能になりました。
Atomics
Atomicsは複数のスレッド間で同じデータを安全に読み書きするための仕組みです。
SharedArrayBufferやArrayBufferと合わせて利用されます。
// 4バイトのSharedArrayBufferを作成
// SharedArrayBufferはcrossOriginIsolatedで安全なコンテキストでのみ実行可能
const sab = new SharedArrayBuffer(4);
// 4バイトのInt32Arrayを作成
const int32 = new Int32Array(sab);
Atomicsは静的なメソッドのみを持つオブジェクトで、共有メモリ上のデータを他の処理に割り込まれずに一度に完了する操作(不可分操作、Atomics操作)を行う関数や複数の実行単位でイベントを待機したり通知したりするための関数を提供します。
不可分操作、Atomics操作
Atomicsが提供する基本的な不可分操作は以下の通りです。一連の処理でint32は共通した変数です。
Atomics.loadは指定したインデックスの値を読み込みます。
// 0番目の値を読み込む
const value = Atomics.load(int32, 0);
console.log(value); // 0
Atomics.storeは指定したインデックスに値を書き込みます。
// 0番目の値に1を書き込む
Atomics.store(int32, 0, 1);
console.log(Atomics.load(int32, 0)); // 1
Atomics.addは指定したインデックスの値に指定した値を加算します。
// 0番目の値に1を加算する
Atomics.add(int32, 0, 1);
console.log(Atomics.load(int32, 0)); // 2
Atomics.subは指定したインデックスの値から指定した値を減算します。
// 0番目の値から1を減算する
Atomics.sub(int32, 0, 1);
console.log(Atomics.load(int32, 0)); // 1
Atomics.andは指定したインデックスの値と指定した値の論理積を計算します。
// 0番目の値と1の論理積を計算する
Atomics.and(int32, 0, 1);
console.log(Atomics.load(int32, 0)); // 1
Atomics.orは指定したインデックスの値と指定した値の論理和を計算します。
// 0番目の値と1の論理和を計算する
Atomics.or(int32, 0, 1);
console.log(Atomics.load(int32, 0)); // 1
Atomics.xorは指定したインデックスの値と指定した値の排他的論理和を計算します。
// 0番目の値と1の排他的論理和を計算する
Atomics.xor(int32, 0, 1);
console.log(Atomics.load(int32, 0)); // 0
Atomics.exchangeは指定したインデックスの値を指定した値で置き換えます。返り値は置き換え前の値です。
// 0番目の値を1で置き換える
const oldValue = Atomics.exchange(int32, 0, 1);
console.log(oldValue); // 0
console.log(Atomics.load(int32, 0)); // 1
Atomics.compareExchangeは指定したインデックスの値が指定した値と等しい場合にのみ、指定した値で置き換えます。返り値は置き換え前の値です。
// 0番目の値が1の場合に2で置き換える
const oldValue = Atomics.compareExchange(int32, 0, 1, 2);
console.log(oldValue); // 1
console.log(Atomics.load(int32, 0)); // 2
イベントの待機と通知
Atomicsはwaitとnotifyによってスレッド間でイベントを待機したり通知したりします。
これらのメソッドはSharedArrayBufferを元にしたInt32ArrayまたはBigInt64Arrayでのみ利用可能です。
waitは指定したインデックスの値が指定した値と一致する場合notifyからの通知があるまで待機します。
待機中、スレッドはブロックされます。そのため、メインスレッドから呼び出すことは許可されていません。
// notifyから通知が来るまで待機する
// 第4引数はタイムアウト時間(ミリ秒)で、デフォルトはInfinityで無限
// 返り値は"ok"、"not-equal"、"timed-out"のいずれか
// "ok": notifyから通知が来て待機が解除された場合
// "not-equal": 指定したインデックスの値が指定した値と異なっていた場合
// "timed-out": タイムアウト時間が経過した場合
Atomics.wait(int32, 0, 0);
// 待機完了後に呼ばれる
console.log('waited');
// 下の例と合わせて実行されたとき
console.log(Atomics.load(int32, 0)); // 1
waitで待機したスレッドが自動で再開することはありません。notifyを使って通知する必要があります。
notifyは指定したインデックスの値で待機しているスレッドに対して通知を送信します。
// 0番目の値を1にする
Atomics.store(int32, 0, 1);
// 0番目の値で待機中のwaitに通知する
// 第3引数には通知する対象の個数、デフォルトはInfinity
const agent = Atomics.notify(int32, 0);
// 通知されたAgentの数を返す
console.log(agent); // 1
上記の2つの例では、待機中のスレッドは、int32の0番目の値に対して通知が送られるのを待っています。
値を1に更新したあとnotifyによって通知が行われ、スレッドは待機状態を抜けます。
Atomics.pause
Atomics.pauseはBaseline 2025で追加された、マイクロウェイト(非常に短い待機)を実現するための新しい静的メソッドです。
このメソッドは、スピンロック、ビジーウェイトのループ内で使用され、CPUに対してスピン中であるというヒントを与え、リソースの消費を最適化します。
let spins = 0;
do {
if (TryLock()) {
return;
}
spins++;
} while (spins < kSpinCount);
上記のようなループは、ロックが解放されるまでCPUをフル稼働させるので、他のプロセスやスレッドに悪影響を及ぼす可能性があります。
let spins = 0;
do {
if (TryLock()) {
// Lock acquired.
return;
}
SpinForALittleBit();
spins++;
} while (spins < kSpinCount);
上記のSpinForALittleBitはロックが解放されるのを待つ間に短時間だけ待機させるように、CPUに対して「現在スピン中である」というヒントを与える関数です。これによりリソースの消費の最適化が行われます。
Atomics.pauseはこれを実現したメソッドになります。
Atomics.pauseには任意引数があり、待機時間についてのヒントを正の整数で渡します(ヒントが利用されることは保証されていません)。利用された場合は、与えられた値より小さい値を渡された時の待機時間以上は待つように働きます。
Intleの最適化マニュアルではループにおいてこれを使ったBackoff戦略を利用することが推奨されています。
おわりに
共有メモリの安全な操作方法として、Atomicsの静的メソッドの基本的な使い方から、新たに提案されているAtomics.pauseの活用までを解説しました。
Atomicsを適切に利用することで、マルチスレッド環境におけるデータの整合性を保ちつつ、効率的な同期処理が可能になります。特にAtomics.pauseがBaseline 2025に導入されたことにより、、スピンロック中のリソース最適化が期待され、より高性能な並列処理が実現できるようになったことには注目です。