Mozilla Hacksの「Avoiding race conditions in SharedArrayBuffers with Atomics」を読みました。SharedArrayBufferとAtomicsを扱う3本シリーズの第3回で、共有メモリを使うときに起きるrace conditionと、それをAtomicsがどう抑えるのかを説明しています。
この記事は、前回紹介したQuantum CSS / Styloの記事からdata raceの背景として参照されていました。StyloはRustでdata raceをコンパイル時に防ぐ話でしたが、今回の記事はJavaScriptの低レベル共有メモリAPIで、開発者が何に気をつけるべきかを説明しています。
記事の要点
- SharedArrayBufferは複数スレッドで同じメモリを共有できるため、race conditionを起こしやすくなります。
- 記事は、アプリ開発者がSharedArrayBufferやAtomicsを直接使うことは想定しにくく、経験のあるライブラリ作者が抽象を作るためのAPIだと説明しています。
- 単一操作に見えるインクリメントも、CPUレベルでは読み込み、計算、書き戻しに分かれるため、スレッド間で混ざると値が壊れます。
Atomics.addなどは、そうした複数ステップの操作を不可分なatomic operationとして扱えるようにします。Atomics.compareExchangeは、値を読んで計算し、他スレッドに更新されていなければ書き戻す、という再試行型の操作に使えます。- 複数操作をまとめて守りたい場合はlockが必要になり、
Atomics.wait、Atomics.wake、compareExchange、storeなどでライブラリ作者がlockを作れます。 - CPUやコンパイラは高速化のために命令を並べ替えることがあり、マルチスレッドではそれがrace conditionになることがあります。
Atomics.loadとAtomics.storeは、メモリ操作の順序を守るためのfenceのような役割を果たします。
リンク先も見てわかったこと
シリーズ前半の「A crash course in memory management」では、メモリを値としてではなく、確保、参照、解放、再利用される資源として見る前提が説明されています。Atomics記事は、この前提の上に「複数の実行単位が同じメモリを見ると何が起きるか」を積み上げています。
第2回の「A cartoon intro to ArrayBuffers and SharedArrayBuffers」は、ArrayBufferとSharedArrayBufferの違いを理解する入口です。共有できることは高速化や並列化につながりますが、共有するからこそ順序や排他制御が必要になる、という流れが第3回へ続いています。
記事内で参照されているjs-lock-and-conditionは、SharedArrayBufferとAtomicsを使ったlockとcondition variableのサンプル実装です。リポジトリは現在inactiveですが、Atomicsが直接アプリロジックを書くためというより、こうした同期プリミティブを作るための部品だと理解しやすいリンクです。
気づき
今回の気づきは、Atomicsの価値は「共有メモリを速く使う」ことよりも先に、「共有メモリを使ったときの壊れ方を制御する」ことにあるという点です。共有メモリはコピーを減らせますが、読み書きの順序が曖昧になった瞬間に、結果は再現しづらいバグになります。
特に印象的だったのは、race conditionが単一操作、複数操作、命令の並べ替えという複数の層で起きることです。インクリメントのような簡単な処理でも壊れますし、オブジェクト全体の更新にはlockが必要になります。さらに、ソースコード上の順序とCPUが実行する順序が一致するとは限らない。Atomicsは、こうした層ごとの危険に対して最低限の同期手段を提供しているわけです。
これはQuantum CSSのRust採用とも対照的です。Rustはdata raceをコードの構造と型システムで防ごうとし、Atomicsは低レベルAPIとして開発者に明示的な同期手段を渡す。どちらも並列処理を可能にする技術ですが、片方は安全側へ寄せた抽象、もう片方は安全な抽象を作るための部品として見えるのが面白いところです。
読むとよさそうな人
- SharedArrayBufferとAtomicsがなぜ難しいのかを知りたい人
- race condition、lock、atomic operation、memory orderingをざっくり整理したい人
- JavaScriptの並列処理APIと、Rustのdata race防止の違いを比較して理解したい人

コメントを残す