JavaScriptでマルチスレッドの実行…
それは Web Worker を使えば可能です。
ただし普通の言語のマルチスレッドと違い、
- 別コードとして切り離して実行
- 呼び出し元と変数の受け渡しが必要
- 他言語のスレッドとは全然違う
そういう違いとか成約がたくさんです。
僕自身も分かっていないことが多かったので、
JavaScriptでのWeb Workerの使い方まとめます。
具体的な応用コード例なども紹介
このページの目次
JavaScript自体はシングルスレッド
初めに大前提になる事実からです。
- JavaScript自体はシングルスレッド
- JavaScriptにマルチスレッド機能はない
たとえば以下のようなコードをよく見かけます。
▼ これはマルチスレッドではない
1 2 3 4 5 6 7 8 9 |
/// 500ミリ秒ごとに実行 setInterval(()=>{ /// 処理1 }, 500); /// 300ミリ秒ごとに実行 setInterval(()=>{ /// 処理2 }, 300) |
これはマルチスレッドではありません。
僕自身もスレッドみたいなものが起動して処理1・処理2が並行実行されると思い込んでました。他言語でいうところのThreadみたいな感じで
でもJavaScriptはシングルタスクです。
▼ 分かりやすい解説。イベントループの話
JavaScriptはシングルスレッドです。一度に実行できるタスクは1つだけです。つまり、
2つ以上の処理を並行して実行できない
2つ以上の関数を同時実行できないということになります。例えば、10秒掛かるタスクを実行すると、他のタスクは10秒間待機しなければなりません。JavaScriptはデフォルトでブラウザのメインスレッドで実行されるため、UI全体が動かなくなります。
引用元 : https://qiita.com/hirokikondo86/items/226905890944603dba39
従来のJSではマルチスレッドは扱えません。
そこに注意が必要です。
マルチスレッドは Web Worker で実現できる
そこで登場したのがWeb Workerの仕組みです。
▼ その意味はMDNなどを見れば早い
ウェブワーカー (Web Worker) とは、ウェブアプリケーションにおけるスクリプトの処理をメインとは別のスレッドに移し、バックグラウンドでの実行を可能にする仕組みのことです。時間のかかる処理を別のスレッドに移すことが出来るため、 UI を担当するメインスレッドの処理を中断・遅延させずに実行できるという利点があります。
引用元 : https://developer.mozilla.org/ja/docs/Web/API/Web_Workers_API
上記にあるように次の利点があります。
- 重い処理をメインと切り離せる
- UIスレッドをブロックしない
これが Web Worker の最大の利点ですね。
Web Worker の基本的な使い方・コード例
ということでWeb Workerの使い方について。
次のような構成を考えてみます。
- index.html
- index.js
- worker.js
上記の index.html、index.js がメインスレッドを担当するJavaScriptコードを含むファイルです。つまりWeb Workerを使わない普通のコードってコト
そして worker.js がWeb Workerによってバックグラウンドで動くスクリプトです。これを index.js から呼び出すことでマルチスレッドのように機能します。
言葉だと分かりにくいので…
コードで示すなら次の通りです。
▼ index.jsのコード例
1 2 3 4 5 6 7 8 9 10 11 12 |
if(window.Worker){ /// Web Workerを取得 const worker = new Worker('worker.js'); /// 引数を渡してスレッド処理開始 worker.postMessage({a:2, b:10}); /// スレッド終了後の結果を受け取る worker.onmessage = (e)=>{ console.log('Result : ', e.data); } } |
▼ worker.jsのコード例
1 2 3 4 5 6 7 8 9 10 11 |
/// メインスレッドから受け取る onmessage = function(e){ const a = e.data['a']; const b = e.data['b']; /// すごく時間のかかる処理… /// メインスレに結果を返す const result = a**b; postMessage(result); } |
これが最もシンプルな使用例です。
普通のJSコード(=シングルスレッドな従来のJS)から 1. new Worker('worker.js') でワーカー生成 => 2. postMessage() で引数渡してスレッド呼び出し => 3. onmessage = (e)=>{...} でスレッドの結果を受け取る
メイン側はこういう流れになってます。
そしてスレッド側の worker.js については 1. onmessage = (e)=>{...} でメイン側の呼び出しを待つ => 2. e.data から渡された引数を解析 => 3. 重い処理とか実行して postMessage() で結果を返す
そういう分かりやすい流れです。
また inde.html から直接呼び出しでもOKです。
Web Workerの実用的な応用コード例
実用的かつ応用的なWeb Workerの使い方
例えば次のような処理をしたいとします。
- フィボナッチ数を求めるWorker作成
- フィボナッチ数の39番目の計算スレッド
- フィボナッチ数の40番目の計算スレッド
- それぞれ結果を受け取ってUIに表示
フィボナッチ数は求めるのに時間がかかり、
Web Wrokerの実用性を確認するのに最適です。
そこでこんなコードを書いてみました。
index.js : UIスレッド側のコード例
次のようなコードです。
▼ index.html
1 2 3 4 5 6 7 |
<button id="btn"> 配列を初期化する </button><br> <div id="ans1"></div> <div id="ans2"></div> <script src="index.js"></script> |
▼ index.js
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 |
const $btn = document.getElementById('btn'); const $ans1 = document.getElementById('ans1'); const $ans2 = document.getElementById('ans2'); if(window.Worker){ $btn.onclick = ()=>{ /// 計算が終わるまでシャッフル表示 const timer1 = shuffle($ans1); const timer2 = shuffle($ans2); /// 2つのWorkerスレッドを用意する const worker1 = new Worker('worker.js'); const worker2 = new Worker('worker.js'); /// 41番目のフィボナッチ数の計算を依頼 const N1 = 41; worker1.postMessage({n: N1}); worker1.onmessage = (e)=>{ /// 計算結果を表示する clearInterval(timer1) ans1.textContent = `fib(${N1}) = `+e.data; } /// 39番目のフィボナッチ数の計算を依頼 const N2 = 39; worker2.postMessage({n: N2}); worker2.onmessage = (e)=>{ /// 計算結果を表示する clearInterval(timer2) ans2.textContent = `fib(${N2}) = `+e.data; } } /// 答え要素でシャッフル表示する function shuffle($elem){ return setInterval(()=>{ $elem.textContent = Math.floor( Math.random()*Number.MAX_SAFE_INTEGER ); }, 20); } } |
これがメインスレッド側のコード
Web Workerは同一スクリプトに対して2つ以上のインスタンスも生成可能です。ここではフィボナッチ数列の38番目と41番目を求めるスレッドを生成しました。
そして結果を受け取って表示してるだけです。
worker.js = フィボナッチ数列の計算スレッド
ということで肝心の worker.js の内容です。
▼ worker.js
1 2 3 4 5 6 7 8 9 10 11 |
onmessage = function(e){ const n = e.data['n']; const result = fib(n); postMessage(result); } function fib(n){ if(n===1||n===2){ return 1; } if(isNaN(n)){ return NaN; } return fib(n-1)+fib(n-2) } |
フィボナッチ数を計算してます。
上記コードでは再帰を使って求めてますが、本当は単純ループでも計算できます。あえてWeb Workerで高負荷処理を処理するために再帰を使いました。
▼ ちなみに再帰なしで計算する方法について
現に fib(41) だと数十秒の計算時間になります。
再帰は最悪だけど、ここでは負荷再現ということで…
上記コードの実行結果(Gif)
実行すると以下の結果になりました。
▼ このような実行結果になった(Gif)
▼ 答えが表示された瞬間
答えをシャッフル表示したのは「Web WorkerスレッドがUIスレッドをブロックしないこと」を視覚化するためです。実際、ワーカーでどれだけ重い処理をしてもbutton・inputなども操作できます。
また2つ以上のスレッドを並列に動かせてます。
Web Workerでは引数を参照渡しできない
Web Workerには成約があります。
それは引数の参照渡しができないこと
例えば先ほどのコードを再掲します。
1 2 3 4 5 |
/// Web Workerを取得 const worker = new Worker('worker.js'); /// 引数を渡してスレッド処理開始 worker.postMessage({a:2, b:10}); |
上記でスレッドに渡された {a:2, b:10} は参照ではなく、コピーされて別オブジェクトで渡されます。それはオブジェクトに限らず配列も同様です。
以下のStack Overflowにもこんな回答がされてます。
▼ 参照渡しってできんの?への回答
It is not possible. You have to send the object, update it in the worker and then return the updated version to the main thread.
引用元 : https://stackoverflow.com/questions/33544994/pass-object-by-reference-from-to-webworker
▼ 上記回答の日本語意訳
それは不可能です。Web Workerに対してオブジェクトを送信し、ワーカー内で更新し、メインスレッドへ更新されたバージョンを返してあげないといけません。
結論としては無理ってこと
メインスレッドとWorkerスレッドはメモリを共有していないため、仮にシンプルなオブジェクトだったとしても参照のように扱われません。
そこがWeb Workerの制約の1つです。
一部のバイナリデータは参照渡しも可能
ただし一部のデータはWorker内でも参照できます。
先ほどのStackOverflowの以下回答に注目です。
Actually yes, it is possible in, (surprise, Surprise!) Chrome 17+ and Firefox 18+ for certain objects (see here).
123456 // Create a 32MB "file" and fill it.var uInt8Array = new Uint8Array(1024 * 1024 * 32); // 32MBfor (var i = 0; i < uInt8Array.length; ++i) {uInt8Array[i] = i;}worker.postMessage(uInt8Array.buffer, [uInt8Array.buffer]);
例えばUint8Arrayみたいな8ビット配列など
詳しく知りたい人はググってください。
ローカルでWeb Workerを動かすには…?
それからWeb Workerでありがちなのが次のこと
ローカル環境で動かすとエラー発生
その回避策とかは次記事でまとめました。
▼ Web Workerをローカル環境で動かす2つの方法
ローカルで動かしたい人は上記を試してください。
Web Workerのブラウザ対応状況
これはほとんど心配する必要ありません。
調べると97%のブラウザで対応済です。
▼ Can i use... で調べる場合
▼ 現時点でも97%のブラウザが対応してる
対応ブラウザは3%を除いた全てです。
以上、Web Workerの使い方とか応用例でした。
JavaScriptではマルチスレッドは使えます。ではまた