【JavaScript】 SharedArrayBufferの使い方とブラウザでの制限
更新日:2024/02/27
SharedArrayBufferを使用すると異なるスレッド間で同じメモリ領域を共有することができます。
使い方によっては効率の良いアプリケーションを作成できます。
しかし脆弱性を指摘されていることから、ブラウザでの使用に制限がかかっています。
スレッド間でデータを共有
JavaScriptは基本的にはシングルスレッドで動作するように設計されています。
しかし、ブラウザのAPIでサブスレッドの作成がサポートされていることで、並列処理が可能となっています。
ブラウザの並列処理については、次のページを読んでみてください。
■【JavaScript】 ブラウザでの並列処理はとても簡単Web Workerの使い方
このAPIではスレッド間で通信したデータを変更したとき、相手側のデータを変更しないような仕組みが組み込まれています。
例えばArrayBuffer(Sharedではない方)は、同じサイズのデータブロックを新しく作成して、元のデータをコピーしています。
一方、SharedArrayBufferは、データブロックをコピーせずに、そのまま渡されます。
そのため、送信側と受け側のスレッドで、同じデータブロックを操作できるようになります。
ようするに二つのスレッドでデータを共有できるのです。
SharedArrayBufferは非常に便利な機能ですが、ブラウザでの使用においてセキュリティの問題があることから制限が設けられ、非常に使いにくくなっています。
そのため使用するときは、本当に使用するべきかどうかを検討してください。
制限についてはSharedArrayBufferの使用制限でお伝えします。
SharedArrayBufferの使い方
SharedArrayBufferはArrayBufferとほぼ同じです。
詳しい使い方は、次のページを読んでみてください。
■【JavaScript】 ArrayBufferとTypedArray-メモリを確保してアクセス
TypedArrayはSharedArrayBufferのインスタンスを渡す
Uint8ArrayなどのTypedArrayは、サイズ指定でインスタンスを作成できます。
しかし内部でArrayBufferが生成されてしまい、SharedArrayBufferは生成できません。
× const sharedArray = new Uint8Array( 100 ); // ArrayBufferが生成される
SharedArrayBufferのインスタンスを渡す必要があります。
〇 const sharedArray = new Uint8Array( new SharedArrayBuffer(100) );
SharedArrayBufferでの簡単な並列処理コード
次のコードはSharedArrayBufferを使用したコードです。
メインスレッドのコード
const ar = new SharedArrayBuffer(10);
const worker = new Worker( "./worker.js");
worker.postMessage(ar);
worker.addEventListener( "message" , m =>{
const pos = m.data;
const u8 = new Uint8Array(ar);
console.log( u8[pos] );
});
ワーカースレッドのコード
addEventListener( "message" , m =>{
const u8 = new Uint8Array(m.data);
u8[5] = 100;
postMessage( 5 );
});
- メイン側の処理
- メインスレッドで10バイトのSharedArrayBufferを作成
- ワーカースレッドを作成してSharedArrayBufferを送信
- ワーカー側の処理
- 受け取ったSharedArrayBufferからUint8Arrayを作成
- 5バイト目に値とセット
- セットした位置をメイン側にメッセージ送信
- メイン側の処理
- SharedArrayBufferからUint8Arrayを作成
- メッセージで受け取った位置を参照
このコードはSharedArrayBufferを、メインとワーカー双方で自由に操作できるということを伝えるために、ムダな処理が入っています。
次のようにSharedArrayBufferのビュー(TypedArrayやDataView)を送信しても動作します。
TypedArrayを送信
const u8 = new Uint8Array(new SharedArrayBuffer(10));
worker.postMessage(u8);
同時変更をAtomicsで回避する
複数のスレッドで共通のデータにアクセスする場合、スレッドセーフなコードを意識する必要があります。
スレッドセーフとは
SharedArrayBufferは複数のスレッドから自由にアクセスできて便利です。
しかし同時に同じ範囲にアクセスしてしまう可能性があります。
例えば4バイトのデータ読み込み中に、他のスレッドが一部を変更してしまうなどのケースです。
読み込み途中でデータが変更されてしまったら意図した値と異なり、それ以降の処理に不具合がでます。
しかも微妙なタイミングでおこるので不具合の再現が難しくて、原因の特定が困難です。
このような状況は避けるべきですね。
複数のスレッドでデータを同時処理しても問題がおこらないことをスレッドセーフというそうです。
実用的なコードを作成するには、スレッドセーフになるように気を配る必要があります。
Atomics APIでアトミック操作をおこなう
JavaScriptはスレッドセーフなコードを作成するために、Atomics APIが用意されています。
このAPIはアトミックな操作をおこなえます。
Atomics API実行中は、対象となる範囲のデータが他のスレッドによって変更されないことが保証されます。
■アトミック操作とは
アトミックには原子という意味が含まれています。
分子を分割していき、それ以上分割できない状態が原子です(厳密にはちがうかもしれません)
アトミック操作は、同じようにこれ以上分割できない操作を意味しています。
その操作内で複雑なことをしていても、他のスレッドやプロセスには一つの処理にしか見えず、操作を妨げることができないということです。
とりあえず使用例を見てみましょう。
Atomics API使用例
const u8 = new Uint8Array(new SharedArrayBuffer(10) );
Atomics.exchange( u8 , 3 , 10 ); // u8 のインデックス3に10をセット
const data = Atomics.load( u8 , 3 ); // u8 のインデックス3から値を取得
Atomicsはインスタンスではなくて、メソッドを直接呼び出します。
値の操作はTypedArrayのインデックス単位でおこないます。
値のセットと取得をおこなうメソッド
Atomics APIで単純な値のセットと取得をおこなうには、次のメソッドを使用します。
説明 | メソッド名 | 戻り値 |
---|---|---|
TypedArrayのindexにvalueをセット | Atomics.exchange( TypedArray , index , value ) | 変更前の値 |
Atomics.store( TypedArray , index , value ) | value | |
TypedArrayのindexから値を取得 | Atomics.load( TypedArray , index ) | 取得値 |
現在の値との演算結果をセットするメソッド
Atomics APIには現在の値と何らかの演算をおこない、その結果をセットするメソッドが用意されています。
次表のメソッドの引数は( TypedArray , index , value )で、TypedArrayのindexにある値とvalueを演算して、indexに格納します。
演算内容 | メソッド名 | 戻り値 |
---|---|---|
加算 | Atomics.add( ) | 変更前の値 |
減算 | Atomics.sub( ) | |
AND(論理積) | Atomics.and( )||
OR(論理和) | Atomics.or( ) | |
XOR(排他的論理和) | Atomics.sub( ) |
現在の値と比較し値をセットするメソッド
次表のcompareExchangeは引数を4つ取ります。
引数:( TypedArray , index , expectedValue , replacementValue )
現在のindexの値がexpectedValueのとき、indexにreplacementValueがセットされます。
演算内容 | メソッド名 | 戻り値 |
---|---|---|
比較 | Atomics.compareExchange( ) | 変更前の値 |
値が変更されるのを待つメソッド
Atomics APIには値が変更されるまで待つメソッドが用意されています。
説明 | メソッド名 | 戻り値 |
---|---|---|
値が異なるとき待つ | Atomics.wait( ) | "ok", "not-equal","timed-out"のどれか |
値変更を通知する | Atomics.notify( ) | 通知を受け取ったスレッドの数 |
これらのメソッドが動作するブラウザは限られていて、現状ではスマホは動作しないようです。
Atomics.wait( Int32Array, index, value , timeout )
waitメソッドの対象となるTypedArrayは、Int32Arrayのみです。
Int32Arrayのindexにある値がvalueのとき、スレッドが停止します。
イベントループに戻るのではなくて、停止です。
ブラウザでは、このメソッドをメインスレッドで使用できません。
timeoutは省略可能です。
Atomics.notify( Int32Array, index, count )
index位置に対してwaitメソッドを実行して停止しているスレッドに、通知します。
通知を受けたスレッドは復帰して、waitメソッドに続くコードを処理します。
countは通知を行うスレッドの最大数です。
省略した場合は、全てに通知します。
使用例
動きを確認できる簡単な例です。
メインスレッド
const i32 = new Int32Array(new SharedArrayBuffer(40));
const worker = new Worker( "worker.js");
worker.postMessage(i32);
worker.addEventListener( "message" , m =>{
console.log( m.data );
});
setTimeout(()=>{
Atomics.store(i32, 0, 10);
Atomics.notify(i32, 0);
},5000)
ワーカースレッド
addEventListener( "message" , m =>{
const data = m.data;
Atomics.wait(data, 0, 0);
postMessage(data[0]);
});
waitメソッドは値が変更されるのを監視しているような印象がありますが、他スレッドからのnotifyメソッドで復帰するだけです。
上のコードではnotifyメソッド実行前にバッファの値を変更していますが、変更しなくても復帰します。
SharedArrayBufferの使用制限
現状のブラウザはSharedArrayBufferを使用すると『ReferenceError: SharedArrayBuffer is not defined』というエラーが発生します。
SharedArrayBufferそのものが、ブラウザで定義されていないのです。
SharedArrayBufferの脆弱性
調べてみると、SharedArrayBufferはSpectreと呼ばれる脆弱性により第三者がメモリの内容を読み取れる可能性があるようです。
ブラウザの拡張機能を使うとWebページにスクリプトを流し込めるので、その件かと思いましたがそういうことではなくて、CPUレベルの問題だそうです。
CPUレベルでは対処が難しいことから、対応策としてSharedArrayBufferを使用できなくしたのです。
その後ChromeやFirefoxで、cross-origin isolationという仕組みを取り入れているサイトでのみ、SharedArrayBufferを使用できるように変更されています。
SharedArrayBufferの有効化
SharedArrayBufferをWebページで使用するには、そのページのヘッダーに次の二つを含める必要があります。
Cross-Origin-Opener-Policy: same-origin Cross-Origin-Embedder-Policy: require-corp
.htaccessでは次のように記述します。
Header set Cross-Origin-Opener-Policy "same-origin"
Header set Cross-Origin-Embedder-Policy "require-corp"
この設定がうまくいけば、SharedArrayBufferを使用できます。
上手くいかない場合は、ブラウザの開発ツールのネットワークタブでヘッダーを上記の情報を受け取っているかを確認します。
キャッシュが効いていることがあるので、そちらの確認もお忘れなく!
受け取ってない場合は、サーバー側でHeadersモジュールが有効になっていない可能性があります。
レンタルサーバーなど自分でシステムを構成できないときは、諦めた方がいいかもしれません。
異なるドメインリソースの読み込み
SharedArrayBufferを有効にすると、異なるドメイン上の画像やスクリプトを読み込めなくなります。
この場合、画像などがあるサーバー側で次のヘッダーを送信するように設定します。
Access-Control-Allow-Origin: *
さらに呼び出し側のタグで、crossorigin属性を追加します。
<img src="https://xxxx.com/xxx.png" crossorigin>
自分の管理していないサーバーの設定を変更するのは難しいです。
そのような外部リソースが必須なときは、SharedArrayBufferを使用できないと思った方がいいですね。
更新日:2024/02/27
関連記事
スポンサーリンク
記事の内容について
こんにちはけーちゃんです。
説明するのって難しいですね。
「なんか言ってることおかしくない?」
たぶん、こんなご意見あると思います。
裏付けを取りながら記事を作成していますが、僕の勘違いだったり、そもそも情報源の内容が間違えていたりで、正確でないことが多いと思います。
そんなときは、ご意見もらえたら嬉しいです。
掲載コードについては事前に動作確認をしていますが、貼り付け後に体裁を整えるなどをした結果動作しないものになっていることがあります。
生暖かい視線でスルーするか、ご指摘ください。
ご意見、ご指摘はこちら。
https://note.affi-sapo-sv.com/info.php
このサイトは、リンクフリーです。大歓迎です。