Subterranean Flower

SharedArrayBufferとAtomics APIを用いてWorker間でデータを共有する

Author
古都こと
ふよんとえんよぷよぐやま!!!

従来のJavaScriptではWorker間でのデータ共有はできませんでしたが、SharedArrayBufferとAtomics APIを用いればそれが実現できるようになりました。

WorkerはJavaScirptでマルチスレッドを実現するための仕組みです。詳しくは「Web Workersを用いてJavaScriptをマルチスレッド化する」をご覧ください。

Workerは元来メッセージのやりとりのみでデータを共有し、同じメモリの値を共有することはできませんでした。しかし近年になって導入されたSharedArrayBufferで、Worker間のメモリ共有が可能になりました。

SharedArrayBuffer

SharedArrayBufferは固定長のTypedArrayです。使用するには、単にバイト長を指定してnewするだけです。

// SharedArrayBufferを作成する。
// SharedArrayBufferは固定長の配列で、コンストラクタにはバイト長を指定する。
// 今回は1バイトのSharedArrayBufferを作成する。
const sBuffer = new SharedArrayBuffer(1);

このとき、SharedArrayBufferの初期値はすべて0です。

また、SharedArrayBufferはそのままでは操作できません。操作のためには、ビューとなるTypedArrayを作成する必要があります。

// SharedArrayBufferを作成する。
// SharedArrayBufferは固定長の配列で、コンストラクタにはバイト長を指定する。
// 今回は1バイトのSharedArrayBufferを作成する。
const sBuffer = new SharedArrayBuffer(1);

// 操作のためのビューを生成する。
// 今回はuint8。
const bufferView = new Uint8Array(sBuffer);
bufferView[0] = 123; // 値を書き込む。

SharedArrayBufferをWorkerに転送する

SharedArrayBufferをWorkerに転送するには、普通にメッセージとして送信するだけです。Worker側ではSharedArrayBufferを受け取り、操作のためのビューを生成します。

// worker

let sBuffer;
let bufferView;

self.addEventListener('message', (msg) => {
    sBuffer = msg.data; // SharedArrayBufferを受け取る。
    bufferView = new Uint8Array(sBuffer); // 操作のためのビューを生成する。
    console.log(bufferView);
});
// main

const worker = new Worker('worker.js');

// SharedArrayBufferを作成する。
// SharedArrayBufferは固定長の配列で、コンストラクタにはバイト長を指定する。
// 今回は1バイトのSharedArrayBufferを作成する。
const sBuffer = new SharedArrayBuffer(1);

// 操作のためのビューを生成する。
// 今回はuint8。
const bufferView = new Uint8Array(sBuffer);
bufferView[0] = 123; // 値を書き込む。

// SharedArrayBufferをメッセージとして送信する。
worker.postMessage(sBuffer);

これでWorkerにSharedArrayBufferを送信し、元のメインスレッド側でも引き続きSharedArrayBufferの値を操作することができます。

SharedArrayBufferはスレッド間で共有されているので、お互いの操作が反映されあう状態になっています。

Atomics API

これでWorker間でSharedArrayBufferを共有することができました。しかし大きな問題があります。これではスレッドセーフではないのです。

スレッドセーフというのは、複数のスレッド(ここではWorker)がデータを同時に処理しても問題ない状態を表します。SharedArrayBufferでは複数のWorkerでデータが共有できるので、複数のWorkerが同時に処理すると、問題が発生することがあります。

スレッドセーフな操作を実現するには、Atomics APIを使用します。Atomics APIを使用すれば、SharedArrayBufferの操作中に他のWorkerが同時に同じ箇所を操作して処理が破綻することを防ぐことができます。

const sBuffer = new SharedArrayBuffer(10);
const bufferView = new Uint8Array(sBuffer);

bufferView[0] = 1;
bufferView[1] = 2;

// Atomics APIを利用してスレッドセーフな操作を行う。
Atomics.add(bufferView, 0, 10); // 0番目の値に、値10を足す。
console.log(Atomics.load(bufferView, 0)); // 11

Atomics APIには、以下の操作があります。

  • Atomics.add(typedArray, index, value)
    • typedArrayのindex番目の値にvalueを足して、index番目にその結果を書き込みます。
  • Atomics.and(typedArray, index, value)
    • typedArrayのindex番目の値とvalueのANDをとって、index番目にその結果を書き込みます。
  • Atomics.compareExchange(typedArray, index, expected, replacement)
    • typedArrayのindex番目の値とexpectedを比較し、等しければindex番目にreplacementを書き込みます。
  • Atomics.exchange(typedArray, index, value)
    • typedArrayのindex番目にvalueを書き込み、書き込む前の値を返します。
  • Atomics.load(typedArray, index)
    • typedArrayのindex番目の値を読み込み返します。
  • Atomics.or(typedArray, index, value)
    • typedArrayのindex番目の値とvalueのORをとり、index番目にその値を書き込みます。
  • Atomics.store(typedArray, index, value)
    • typedArrayのindex番目にvalueを書き込みます。
  • Atomics.sub(typedArray, index, value)
    • typedArrayのindex番目の値からvalueを引き、index番目にその結果を書き込みます。
  • Atomics.xor(typedArray, index, value)
    • typedArrayのindex番目の値とvalueのXORをとり、index番目にその結果を書き込みます。

また、加えて以下の操作が利用できます。

  • Atomics.wait(int32Array, index, value[, timeout=Infinity])
    • 共有されたint32Arrayのindexにスレッドを紐付け、valueが保存されているか検証し、スレッドをスリープするか、タイムアウトします。値がvalueと異なる場合はすぐさま’not-equal’を返し、スレッドが起こされると’ok’を返します。タイムアウトした場合は’timed-out’を返します。主なブラウザでは、メインスレッドではこのメソッドを使用できません。
  • Atomics.wake(int32Array, index[, count=Infinity])
    • 共有されたint32Arrayのindexに紐付けられたスレッドを、count個起こします。
  • Atomics.isLockFree(size)
    • BYTES_PER_ELEMENT == sizeの配列上のAtomic演算が、ロック機構を使用しない場合trueを返します。それ以外の場合はfalseを返します。

Atomics APIについて、詳しくはMDNのAtomicsのページをご覧ください。