JS.next

JavaScriptの最新実装情報を追うブログ

SharedArrayBufferとAtomics APIについて

概要

JSで大きな処理を効率良く捌きたい時、今までもWorker等でスレッド立てて処理を分割する事はできたが、
スレッド間のやり取りの方法は制限されたものしかなく、バッファを共有することもできなかった。
そこで新しく導入されたSharedArrayBufferを用いると、スレッド間で共同利用できるバッファを作る事ができる。


記事更新履歴

※この記事はV8が仕様の新しいバージョンを実装するのに合わせて断続的に更新していきます。

[2016/07/19] V8の半年ぶりの新仕様追従に対応

[2015/09/30] 公開


通常のArrayBufferとの比較

前準備:

//  メッセージを受け取ると渡された型付配列のインデックス0を123にするWorker
w = new Worker(URL.createObjectURL(new Blob([`
  self.onmessage = e => {
    e.data[0] = 123
  }
`])))


従来型1:

ta = new Uint8Array(1)
console.log(ta[0])  // 0
w.postMessage(ta)   // 型付配列のバッファが複製される
setTimeout(() => console.log(ta[0]), 100)  // 0
                    // 複製したものを操作しても当然元には影響がない


従来型2:

ab = new ArrayBuffer(1)
ta = new Uint8Array(ab)
console.log(ta[0])       // 0
w.postMessage(ta, [ab])  // 型付配列のバッファが移譲される
setTimeout(() => console.log(ta[0]), 100)  // undefined
                         // 移譲したので元は空になってしまう


新型:

sab = new SharedArrayBuffer(1)
sta = new Uint8Array(ab)
console.log(sta[0])        // 0
w.postMessage(sta, [sab])  // 型付配列のバッファが共有される
setTimeout(() => console.log(sta[0]), 100)  // 123
                           // 共有先の操作は当然元に影響する


Atomics API

Atomics APIを使うとSharedArrayBufferの操作やスレッド制御ができる。

Atomics.

load( ita, i )

 共有符号付き整数型配列 ita のインデックス i を読み出す

store( ita, i, v )

 ita[i] に値 v を書き込む

exchange( ita, i, v )

 ita[ i ] を読み出し、ita[ i ] に v を書き込み、最初に読みだした値を返す処理を排他的に行う
 このメソッドや以下の同様のメソッドを使わずに同じことをしようとした場合、
 読み出しと書き込みの間に他のスレッドが読み書きを行い競合する危険性がある

compareExchange( ita, i, e, v )

 ita[ i ] を読み出し、もしそれが e と等しければ v を書き込み、最初に読みだした値を返す処理を排他的に行う

add( ita, i, v )

 【 ita[ i ] + v 】ita[ i ] を読み出し、それに v を加算した値を書き込み、最初に読みだした値を返す処理を排他的に行う

sub( ita, i, v )

 【 ita[ i ] - v 】

and( ita, i, v )

 【 ita[ i ] & v 】

or( ita, i, v )

 【 ita[ i ] | v 】

xor( ita, i, v )

 【 ita[ i ] ^ v 】

isLockFree( size )

 BYTES_PER_ELEMENT が size である共有型付配列に対し上記の共有型付配列を操作するメソッドが働くとき、
 メソッドが操作するバッファの範囲は当然他のスレッドからの操作を介入させないようロックする必要があるが、
 それ以外の範囲までロックせずに済むかどうかの真偽値を返す(ハード&実装依存
 この値が false な環境では多スレッドで共有バッファを効率良く扱うことができないため非常に利用効率が下がる。

wait( i32a, i, v, timeout = Infinity )

 共有Int32型配列 i32a のインデックス i にスレッドを紐付けて休止状態に入る
 ただし「 i32a[ i ] != v 」の場合は直ぐに「 “not-equal” 」が返される
 timeout ミリ秒以内にスレッドが起こされると「 “ok” 」が返され、
 それを過ぎると「 “timed-out” 」が返され、スレッドがレジュームされる

wake( i32a, i, count = Infinity )

 i32a のインデックス i に紐付けられているスレッドを最大 count 個起こす
 実際にスレッドを起こした数値が返される


利用例

//  メッセージを受け取ると渡された型付配列のインデックス0に紐付けて最大500ミリ秒休止状態に入る
w2 = new Worker(URL.createObjectURL(new Blob([`
  self.onmessage = e => {
    let sta = e.data
    let v = Atomics.wait(sta, 0, 0, 500)
    console.log('resumed', v)
  }
`])))
sab2 = new SharedArrayBuffer(4) 
sta2 = new Int32Array(sab2)

setTimeout(()=>    w2.postMessage(sta2, [sab2])     ,   0 )
setTimeout(()=>    Atomics.wake(sta2, 0, 10)   , 100 )
                   // "resumed" "ok"

setTimeout(()=>    w2.postMessage(sta2, [sab2])     , 200 )
                   // "resumed" "timed-out"

setTimeout(()=>   sta2[0] = 123                     , 800 )
setTimeout(()=>   w2.postMessage(sta2, [sab2])      , 900 )
                  // "resumed" "not-equal"


概ねきちんと動くようになるバージョン

V8 4.7
Chrome 47


参考外部リンク