前回からのつづき
前回、メインスレッドと Web Worker とのデータの受け渡し方法として、転送による受け渡しについて説明してきました。
転送での受け渡しでは、メインスレッドと Web Worker との間で共通のメモリを所有権を受け渡すことにより、排他的にアクセスできることを説明してきました。
今回は、第3の受け渡しである、共有メモリを使用したデータの受け渡しを中心に説明していきます。
共有メモリ領域( SharedArrayBuffer )を使用したデータの受け渡し
メインスレッドと Web Worker との間には、コピー、転送というデータの受け渡し以外に、もう1つ異なるデータの受け渡し方法が存在します。
それは、共有メモリ領域( SharedArrayBuffer )を使用したデータの受け渡しです。
SharedArrayBuffer は、ArrayBuffer と同様に、固定長のバイト列を保持できるオブジェクトです。
ArrayBuffer オブジェクトとは異なり、SharedArrayBuffer オブジェクトは、複数の実行コンテキスト(Main スレッドや Web Worker など)から同じメモリ領域を共有できるという特徴があります。
つまり、ArrayBuffer の転送による共有では、アクセスが排他的であったにも関わらず、SharedArrayBuffer を使った共有では、同時にアクセスが可能となるということです。
メッセージ送信前
| 受け渡し | メインスレッド | Web Worker |
|---|---|---|
| ArrayBufferの転送 | 読み書き〇 | 読み書き✖ |
| SharedArrayBuffer | 読み書き〇 | 読み書き✖ |
メッセージ送信後
| 受け渡し | メインスレッド | Web Worker |
|---|---|---|
| ArrayBufferの転送 | 読み書き✖ | 読み書き〇 |
| SharedArrayBuffer | 読み書き〇 | 読み書き〇 |
SharedArrayBuffer の受け渡しもコピーではない為、新たなメモリの確保は行われません。
大きく異なるのは、受け渡しが完了した後、メインスレッド側でも通常通り、読み書きが可能であるという点です。
SharedArrayBuffer に対して、メインスレッドと Web Worker は、
メインスレッド
▲
│
▼
┌───────────────────┐
│ SharedArrayBuffer │
└───────────────────┘
▲
│
▼
Web Worker
双方の SharedArrayBuffer オブジェクトは別インスタンスですが、同じ共有メモリを参照しています。
例:メインスレッドから Web Worker に共有メモリの buffer オブジェクトを受け渡す場合、
Main.js
const buffer = new SharedArrayBuffer(4);
const view = new Uint8Array(buffer);
worker.onmessage = (event)=> {
console.log(view[0]);
}
view[0] = 100;
worker.postMessage(buffer);
// コピーも転送も行われず、共有メモリを参照する SharedArrayBuffer が Web Worker 側へ渡される
Worker.js
self.onmessage = (event)=>{
const view = new Uint8Array(event.data);
view[0] = 200;
self.postMessage("SUCCESS");
}
結果 200 となり、Web Worker で書き換えられた値が見えます。
※ SharedArrayBuffer の受け渡しでは、postMessage() の第2引数も必要ありません。
SharedArrayBuffer のセキュリティ上の制限
SharedArrayBuffer は高性能ですが、過去に CPU の脆弱性( Spectre:CPU の投機実行(Speculative Execution)の仕組みを利用した攻撃 )を利用した情報漏えいの問題が見つかりました。
その関係で、2018年には、ほぼ全ブラウザで SharedArrayBuffer が無効化されました。
ただ、その結果、その機能に依存していた
- ゲーム
- WebAssembly
- CAD
- 動画編集
- SQLite WASM
などが困ることになります。
そこで、完全に廃止するのではなく、安全なページだけ使えるようすることで、再度、 SharedArrayBuffer が有効化されることになりました。
この、安全なページだけ使えるようする仕組みのことを クロスオリジン分離(Cross-Origin Isolation) と呼びます。
そのため現在は、多くのブラウザで、利用する際に、クロスオリジン分離(Cross-Origin Isolation)が必要になっています。
正確に言うと、SharedArrayBufferが利用できる条件は、
- セキュアコンテキスト(Secure Context)であること( HTTPS 使用必須 )
- クロスオリジン分離(Cross-Origin Isolation)が有効であること
の両方です。
つまり、ほとんどの環境では、単にコードを書くだけでは使用できなくなっています。
一方、ArrayBufferにはそのような制限はありません。
※ HTTPでは、通常 http://example.com などのセキャアでないコンテキストでは、 SharedArrayBuffer は利用できませんが、開発用途として、http://localhost や http://127.0.0.1 は例外的にセキュアコンテキストとして扱われます。そのため、ローカル開発ではHTTPS証明書がなくても試せることがあります。
クロスオリジン分離(Cross-Origin Isolation)とは
クロスオリジン分離(Cross-Origin Isolation)とは、他のオリジン(サイト)から自分のページを隔離し、安全な実行環境を作る仕組みのことをいいます。
例えば、 https://example.com というサイトがあるとします。
普通は、example.com のページの中には、
- JavaScript
- 画像
- フォント
- iframe
- 広告
- CDN
など、様々なオリジンのデータが混在しています。
example.com
├── script (example.com)
├── image (cdn.example.com)
├── iframe (google.com)
├── font (fonts.example.com)
└── analytics (analytics.example.com)
複数のサイトのデータが混在しているこの状態は、本当に安全なのかどうかが、判別できません。
そこで、サーバーから送付する HTTPヘッダー に特別な設定を行い、それを受け取ったブラウザが制限を設けることで、「安全な実行環境」を実現する仕組みが導入されました。
それが、Cross-Origin Isolation です。
具体的には、下記の値を HTTPヘッダー に設定します。
- Cross-Origin-Opener-Policy: same-origin
- Cross-Origin-Embedder-Policy: require-corp
■ Cross-Origin-Opener-Policy: same-origin
これを COOP と略します。
意味は、「他のオリジンとはブラウザの実行コンテキストを共有しない」です。
■ Cross-Origin-Embedder-Policy: require-corp
これを COEP と略します。
意味は、「読み込むリソースも、安全だと確認できるものだけに限定する」です。
COEP を有効にすると、他オリジンのリソースは、CORP や CORS により共有が許可されたものだけ読み込めるようになります。
■ クロスオリジン分離には、両方の設定が必要
COOP + COEP の両方が有効になって初めて、crossOriginIsolated === true の状態となり、クロスオリジン分離が有効になります。
その結果、安全に
- SharedArrayBuffer
- Atomics.wait()
- OPFS の同期アクセス API(
createSyncAccessHandle()) - WebAssembly Threads
などが使えるようになります。
共有メモリ領域( SharedArrayBuffer )による共有の危険性
まず理解しないといけないことは、メインスレッドと Web Worker は全く別の実行環境で並列動作しているということです。
つまり、メモリの同じ場所に対して、同時に書き換えが起こる可能性があるということです。
これは、協調して動作する場合に、致命的な問題を引き起こす可能性があります。
例えば、同じ共有メモリ領域( SharedArrayBuffer )に対して、
main.js
view[0]++;
worker.js
view[0]++;
という処理を行っていた場合に、両方が同時に実行すると、処理結果を下記のように期待していたところ、
100
↓
101
↓
102
実際には、
100
Main 読む → 100
Worker 読む → 100
Main 書く → 101
Worker 書く → 101
と処理され、102 になるはずが 101 になることがあります。
これを Race Condition(競合)と呼びます。
このような問題を防ぐ為には、メインスレッドと Web Worker の間での何らかの排他制御が必要となります。
共有メモリ領域( SharedArrayBuffer )には、前述のような問題を防ぐために、Atomics APIが用意されています。
※ 尚、Atomics API の使用は競合する部分のみとし、メインスレッドと Web Worker が同時にアクセスする可能性がない部分に関しては、view[0] といった通常の配列指定での読み書きでも特に問題はありません。
Atomics API とは
競合を防ぐために、共有メモリへの1回の操作を、途中で割り込まれない形で実行するための API です。
例えば、
Atomics.add(view, 0, 1);
これは、
view[0] += 1;
ではありません。内部では、
読む
↓
加算
↓
書く
を絶対に途中で割り込まれないように実行します。
これを Atomic(不可分)と呼びます。
このように、Atomics API は、共有メモリ領域( SharedArrayBuffer )に直接アクセスする場合と異なり、排他的なアクセスを可能する 同期アクセス 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()
(つづく)