JavaScript Web Workers:安全な同時実行

Webワーカーは、メインスレッドの外部でJavaScriptコードを実行するツールをプログラマーに提供します。これにより、ブラウザーでの処理が行われます。 このストリームは、画面へのデータ出力の要求を処理し、ユーザーの操作、特にキーストロークとマウスクリックの認識をサポートします。 同じスレッドが、たとえばAJAX要求の処理など、ネットワークのサポートを担当します。



イベントとAJAXリクエストの処理は非同期で、メインスレッドの外部でコードを実行する方法と考えることができますが、このような操作を実行するための負荷はすべてメインスレッドにかかっており、ユーザーインターフェイスの通常の操作を保証するには、これらの操作を実行する必要がありますとても速い。 そうしないと、インタラクティブなページ要素が期待どおりに機能しません。



画像



Webワーカーを使用すると、JavaScriptコードをメインスレッドと通常何が起こるかとは完全に独立した別のスレッドで実行できます。



最近、Webワーカーの助けを借りてどのような実用的なタスクを解決できるかについて多くの話があります。 普通の現代のパーソナルコンピューターでさえも持つ計算能力と、モバイルデバイスがパフォーマンスとメモリサイズの点でそれに近づいているという事実を考えると、ブラウザーアプリケーションでは、以前は複雑すぎると考えられていた多くのことができます。



本日公開する翻訳であるこの資料では、メインスレッドには重すぎるタスクを解決するためにWebワーカーを使用する機能について説明します。 特に、ここでは、メインストリームとWebワーカーストリーム間のデータ交換を整理する方法について説明します。 また、Webワーカーを使用するためのさまざまなシナリオを示すいくつかの例を取り上げます。



Web Workerの基本



多くの場合、アプリケーションのパフォーマンスは、開発者のコ​​ンピューター上の分離された環境で分析され、取得したものに満足したままです。 たとえば、このアプローチでは、最小限の追加プログラムがそのようなコンピューターで実行されます。 ただし、実際にはそうではありません。 通常のユーザーがプログラムと一緒にさらに多くのアプリケーションを実行できるとしましょう。



その結果、Webワーカーによって作成された個別のスレッドを使用せずに、隔離された環境で正常に動作するアプリケーションは、実際の使用シナリオで適切に見えるようにそのようなスレッドを必要とする場合があります。



Webワーカーを実行すると、適切なオブジェクトが作成され、JavaScriptコードでファイルへのパスが渡されます。



new Worker('worker-script.js')
      
      





作成後、ワーカーはメインスレッドから独立した別のスレッドで動作し、ファイルとして転送されるコードを実行します。 ブラウザは、Webワーカーの作成時に指定されたファイルを検索するときに、現在のHTMLページが置かれているフォルダーをルートとする相対パスを使用します。



ワーカーとメインストリーム間のデータは、2つの補完的なメカニズムを使用して送信されます。





message



イベントハンドラーはイベント引数を受け入れ、他のハンドラーと同じように動作します。 この引数には、受信側に渡されるデータを含むdata



プロパティがあります。



上記のメカニズムを使用すると、双方向の情報交換を整理できます。 メインスレッドのコードは、 postMessage()



関数を使用してメッセージをワーカーに送信できます。 ワーカーは、ワーカー環境でグローバルに利用可能なpostMessage()



実装を使用して、メインスレッドに応答を送信できます。



メインスレッドとWebワーカー間でデータを共有するための簡単なフローチャートは次のようになります。 これは、HTMLページのコードがどのようにワーカーにメッセージを送信し、応答を待機するかを示しています。



 var worker = new Worker("demo1-hello-world.js"); //  ,    postMessage()  - worker.onmessage = (evt) => {   console.log("Message posted from webworker: " + evt.data); } //   - worker.postMessage({data: "123456789"});
      
      





以下は、ページからのメッセージの処理と応答を送信するメカニズムが編成されているWebワーカーのコードです。



 // demo1-hello-world.js postMessage('Worker running'); onmessage = (evt) => {   postMessage("Worker received data: " + JSON.stringify(evt.data)); };
      
      





このコードを実行すると、コンソールに次が表示されます。



 Message posted from webworker: Worker running Message posted from webworker: Worker received data: {"data":"123456789"}
      
      





Webワーカーを使用する場合、それらは長時間実行されることが期待され、短いタスクを完了するために使用されず、常に開始および停止されます。 ワーカーのライフサイクル中に、メインスレッドとの多くのメッセージングセッションを行うことができます。 Webワーカーの実装は、2つのメカニズムにより、安全で競合のないコード実行を提供します。





各ワーカースレッドには、HTMLページにあるコードが実行されるJavaScript環境とは異なる個別の分離されたグローバル環境があります。 ワーカーは、ページ環境から利用可能なメカニズムにアクセスできません。 DOMにアクセスできず、 window



およびdocument



オブジェクトを操作できません。



ワーカーには、開発者のコ​​ンソールにメッセージを記録するためのconsole



オブジェクトや、AJAXリクエストを実行するためのXMLHttpRequest



オブジェクトなど、いくつかのメカニズムの独自のバージョンがあります。 ただし、他の問題では、ワーカーによって実行されるコードは自給自足であることが期待されます。 そのため、たとえば、メインストリームで使用する予定のワーカーストリームのdata



は、 postMessage()



関数を介してdata



オブジェクトとして転送する必要があります。



さらに、 postMessage()



関数を使用して送信されたデータはコピーされます。つまり、メインストリームによって行われたこのデータへの変更は、ワーカーストリームの元のデータに影響しません。 これは、メインストリームとワーカーストリーム間で送信される競合する並列データ変更に対する保護の内部メカニズムです。



Web Workerの使用オプション



Webワーカーの一般的な使用法は、メインスレッドに含まれる計算量の点で困難になる可能性のあるタスクです。 この複雑さは、過剰なプロセッサリソースの消費、またはこのタスクの実装が、たとえばデータにアクセスするために予想外に長い時間を必要とする可能性があるという事実のいずれかで表されます。



Webワーカーを使用するためのオプションの一部を次に示します。





最も単純なケースでは、Webワーカーを使用して解決する問題を選択するとき、それを解決するために必要な計算量に注意を払う必要があります。 ただし、たとえばネットワークリソースにアクセスするために必要な時間を考慮することは非常に重要です。 非常に多くの場合、インターネット上のデータ交換セッションはごくわずかな時間、ミリ秒しかかかりませんが、ネットワークリソースが利用できなくなる場合があり、接続が復元されるか、リクエストがタイムアウトするまでデータ交換が停止する場合があります(1-2分かかる場合があります)



そして、隔離された開発環境でプログラムをテストするときにコードを実行するのに時間がかかりすぎない場合でも、ユーザーのコンピューターで多くのタスクが実行される場合に加えて、実際の状態でコードを実行すると問題になります。



次の例は、Webワーカーのいくつかの実用的な使用例を示しています。



ゲーム内の衝突処理



最近、ブラウザーで実行されるHTML5ゲームは非常に一般的です。 中心的なゲームメカニズムの1つは、ゲーム世界でのオブジェクトの動きと相互作用の計算です。 一部のゲームでは、移動する要素の数が比較的少なく、アニメーション化することは難しくありません(たとえば、このバージョンのSuper Marioなど )。 ただし、より集中的なコンピューティングを必要とするゲームがあると仮定しましょう。



この例では、多くのカラフルなオブジェクト(ボールまたはボールと見なします)が表示されます。これらは閉じた長方形のスペースにあり、移動して壁を跳ね返ります。 私たちのタスクは、まずボールがこのスペースから出ないようにし、次にボールが互いに跳ね返るようにすることです。 つまり、相互の衝突と競技場の境界との衝突を処理する必要があります。



境界線との衝突の処理は比較的単純なタスクであり、深刻な計算は必要ありませんが、このタスクの複雑さは詳細に入らない場合でもオブジェクトの数の2乗に比例するため、オブジェクト同士の衝突を検出するには多くのコンピューティングリソースが必要になる場合があります つまり、 n



ボールについて、交差するかどうかを理解するために、他のすべてと比較してそれぞれのボールの位置を確認する必要があり、移動方向を変更する必要がないため、リバウンドを実現し、nの2乗に等しい操作数につながります。



したがって、50個のボールについては、約2500個の比較を行う必要があります。 100個のボールの場合、すでに10,000個のチェックが必要です(実際、ボールn



とボールm



の衝突をチェックする場合、ボールm



とボールn



衝突をチェックする必要はないため、この数は示されている数の半分よりわずかに少なくなっていますが、これにもかかわらず、このような問題を解決するには大量の計算が必要になります)。



この例では、ボール同士の衝突や競技場の境界との衝突を処理するための計算は、個別のWebワーカースレッドで実行されます。 このストリームは1秒あたり60回アクセスされます。これは、ブラウザーのアニメーション速度、またはrequestAnimationFrame()



各呼び出しに対応しrequestAnimationFrame()



。 ここでは、 Ball



オブジェクトのリストを含むWorld



オブジェクトについて説明します。 各Ball



オブジェクトには、現在の位置と速度に関する情報が保存されます(オブジェクトの半径と色に関する情報もあり、画面に表示することができます)。



現在の位置でのボールの出力は、メインスレッド( Canvas



オブジェクトとその描画コンテキストにアクセスできる)で実行されます。 ボールの位置は、Webワーカースレッドで更新されます。 ボールが競技場の境界または他のボールと衝突すると、速度(特に、ボールの移動方向)が変化します。



World



オブジェクトは、ブラウザーのクライアントコードとワーカースレッドの間で渡されます。 これは、数百個のボールに対しても比較的小さなオブジェクトです(たとえば、100個のボールに対して、1つに約64バイトのデータが必要な場合、合計量は約6400バイトになります)。 その結果、ここでの主な問題は、ゲームオブジェクトに関するデータの転送ではなく、システムの計算負荷です。



この例の完全なコードはここにあります 。 特に、アニメーション化されたオブジェクトを表すために使用されるBall



クラスと、アニメーションを実行するmove()



およびdraw()



メソッドを実装するWorld



クラスがあります。



ワーカーを使用せずにアニメーションを実行する場合、この例のメインコードは次のようになります。



 const canvas = $('#democanvas').get(0),   canvasBounds = {'left': 0, 'right': canvas.width,       'top': 0, 'bottom': canvas.height},   ctx = canvas.getContext('2d'); const numberOfBalls = 150,   ballRadius = 15,   maxVelocity = 10; //   World const world = new World(canvasBounds), '#FFFF00', '#FF00FF', '#00FFFF']; //   Ball   World for(let i=0; i < numberOfBalls; i++) {   world.addObject(new Ball(ballRadius, colors[i % colors.length])           .setRandomLocation(canvasBounds)           .setRandomVelocity(maxVelocity)); } ... //   function animationStep() {   world.move();   world.draw(ctx);   requestAnimationFrame(animationStep); } animationStep();
      
      





requestAnimationFrame()



を使用して、画面更新期間の一部として、 animationStep()



関数を1秒あたり60回呼び出します。 アニメーションのステップは、各ボールの位置(および場合によってはその動きの方向move()



を更新するmove()



メソッドの呼び出しと、 canvas



オブジェクトを使用して新しい位置にボールを表示するdraw()



メソッドの呼び出しで構成されます。



このプログラムでワーカーフローを使用するには、 move()



メソッドが呼び出されたときに実行される計算、つまりWorld.move()



コードをワーカーに送信する必要があります。 World



オブジェクトは、 postMessage()



呼び出しを使用して、 data



オブジェクトの形式でワーカースレッドにdata



ます。これにより、ここでmove()



メソッドを呼び出すことができます。 明らかに、 World



オブジェクトをメインストリームとWebワーカーの間で転送する必要があります。これは、画面に表示されるBall



オブジェクトのリストと、それらが保持される長方形領域に関するデータが含まれているためです。 さらに、 Ball



オブジェクトには、それぞれのボールの位置、速度、および移動方向に関するすべての情報が含まれています。



Web Workerを使用するように設計されたプロジェクトを変更すると、アニメーションループは次のようになります。



 let worker = new Worker('collider-worker.js'); //   draw worker.addEventListener("message", (evt) => {   if ( evt.data.message === "draw") {       world = evt.data.world;       world.draw(ctx);       requestAnimationFrame(animationStep);   } }); //   function animationStep() {   worker.postMessage(world);  // world.move() in worker } animationStep();
      
      





ワーカーコードは次のようになります。



 // collider-worker.js importScripts("collider.js"); this.addEventListener("message", function(evt) {   var world = evt.data;   world.move();   //     ,       this.postMessage({message: "draw", world: world}); });
      
      





ここに示すコードは、ウェブワーカースレッドがメインストリームからpostMessage()



を使用して渡されたWorld



オブジェクトを受け取り、ゲームオブジェクトの位置と速度の新しい値を計算した後、同じオブジェクトをメインストリームに戻すという事実に基づいています世界の。 ブラウザがスレッド間で転送されるときにこのオブジェクトのコピーを作成することに注意してください。 ここでは、 World



オブジェクトのコピーを作成するのに必要な時間がO(n ** n)よりはるかに短い、つまり、衝突を検出するのに必要な時間(実際、 World



オブジェクトには比較的少量のデータが格納される) )



ただし、新しいコードを開始すると、予期しないエラーが発生します。



 Uncaught TypeError: world.move is not a function at collider-worker.js:10
      
      





postMessage()



関数を使用してオブジェクトを渡すときにオブジェクトをコピーするプロセスでは、データはオブジェクトのプロパティからコピーされますが、プロトタイプはコピーされません。 World



オブジェクトのメソッドは、オブジェクトがコピーされてワーカーに渡されるときにプロトタイプから分離されます。 これは、構造クローニングアルゴリズムの一部であり 、メインストリームとWebワーカー間でオブジェクトを転送するときにオブジェクトをコピーする標準的な方法です。 このプロセスは、 シリアル化とも呼ばます。



上記のエラーを取り除くために、 World



クラスにメソッドを追加してその新しいインスタンスを作成し(メソッドを持つプロトタイプがあります)、 postMessage()



を使用して送信されたデータに基づいてこのオブジェクトのプロパティを再割り当てします。



 static restoreFromData(data) {   //     ,          let world = new World(data.bounds);   world.displayList = data.displayList;   return world; }
      
      





これらの変更後にコードを実行しようとすると、別の同様のエラーが発生します。 実際には、 World



オブジェクトに格納されているBall



オブジェクトのリストも復元する必要があります。



 Uncaught TypeError: obj1.getRadius is not a function at World.checkForCollisions (collider.js:60) at World.move (collider.js:36)
      
      





World



クラス自体の復元と同じ方法で、 postMessage()



渡されたデータに基づいて各Ball



オブジェクトが復元されるように、 World



クラスの実装を拡張する必要があります。



これで、 World



クラスはWorld



ようになります。



 static restoreFromData(data) {   //     ,          let world = new World(data.bounds);   world.animationStep = data.animationStep;   world.displayList = [];   data.displayList.forEach((obj) => {       //    Ball       let ball = Ball.restoreFromData(obj);       world.displayList.push(ball);   });   return world; }
      
      





同様のrestoreFromData()



メソッドがBall



クラスに実装されています:



 static restoreFromData(data) {   //     ,          const ball = new Ball(data.radius, data.color);   ball.position = data.position;   ball.velocity = data.velocity;   return ball; }
      
      





これらの変更により、アニメーションが正しく実行され、ワー​​カーフロー内のおそらく数百のボールのそれぞれの変位が計算され、ブラウザーの新しい位置に毎秒60回の速度で表示されます。



このWebワーカースレッドの使用例は、メモリではなく大きな計算リソースを必要とするタスクのソリューションを示しています。 解決するために大量のメモリを必要とする問題に直面した場合はどうなりますか?



しきい値画像処理



この例では、プロセッサとメモリの両方に大きな負荷をかけるアプリケーションを検討します。 HTML5 canvas



オブジェクトとして表される画像からピクセルデータを取得して変換し、それらに基づいて別の画像を作成します。



ここでは、2012年にIlmari Heikinenが作成した画像処理ライブラリを使用します 。 プログラムはカラー画像を受け入れ、それをバイナリの白黒画像に変換します。 変換中にグレーのしきい値が使用されます。グレーの色の値がこのしきい値よりも小さいピクセルは黒になり、大きな値のピクセルは白になります。



新しい画像を取得するためのコードは、すべてのカラー値(RGB形式で表示)を通過し、式を使用して対応するグレーの濃淡に変換します。その後、結果のピクセルが黒か白かを決定します。



 Filters.threshold = function(pixels, threshold) {   var d = pixels.data;   for (var i=0; i < d.length; i+=4) {       var r = d[i];       var g = d[i+1];       var b = d[i+2];       var v = (0.2126*r + 0.7152*g + 0.0722*b >= threshold) ? 255 : 0;       d[i] = d[i+1] = d[i+2] = v   }   return pixels; };
      
      





これが元の画像です。









ソース画像



処理後の処理を次に示します。









処理された画像



サンプルコードはこちらにあります



小さな画像を扱う場合でも、処理する必要のあるデータの量と処理の計算コストは​​非常に大きくなる可能性があります。 たとえば、640x480ピクセルの画像には307,200ピクセルがあり、各ピクセルは4バイトのRGBAデータに対応します(Aは色の透明度を設定するアルファチャネルです)。 その結果、このようなイメージのサイズは約1.2 MBです。 Webワーカーを使用して、ピクセルデータを反復処理し、その色の値を変換する計画です。 画像のピクセルデータはメインストリームからWebワーカーに転送され、変更された画像はワーカーからメインストリームに返されます。 メインストリームとワーカーストリームの境界を越えるたびにこのデータをコピーする必要がなければ、いいでしょう。



postMessage()



関数は、メッセージで参照によって送信されるデータを記述する1つ以上のプロパティを設定することにより使用できます。 つまり、データのコピーは送信されず、それらへのリンクが送信されます。 次のようになります。



 <div style="margin: 50px 100px">   <img id="original" src="images/flmansion.jpg" width="500" height="375">   <canvas id="output" width="500" height="375" style="border: 1px solid;"></canvas> </div> ... <script type="text/javascript"> const image = document.getElementById('original'); ... //    HTML5 canvas     const tempCanvas = document.createElement('canvas'),   tempCtx = tempCanvas.getContext('2d'); tempCanvas.width = image.width; tempCanvas.height = image.height; tempCtx.drawImage(image, 0, 0, image.width, image.height); const imageDataObj = tempCtx.getImageData(0, 0, image.width, image.height); ... worker.addEventListener('message', (evt) => {   console.log("Received data back from worker");   const results = evt.data;   ctx.putImageData(results.newImageObj, 0, 0); }); worker.postMessage(imageDataObj, [imageDataObj.data.buffer]); </script>
      
      





ここでは、 Transferable



インターフェースを実装する任意のオブジェクトを使用できます。 data.buffer



オブジェクトのdata.buffer



の構築は、この要件を満たしますUint8ClampedArray



Uint8ClampedArray



(この型の配列は、8ビットの画像データを格納するように設計されています)。 ImageData



context



HTML5 canvas



オブジェクトのcontext



に対してgetImageData()



メソッドが呼び出すものです。



Transferable



インターフェースは、 ArrayBuffer



MessagePort



、およびImageBitmap



いくつかの標準データ型によって実装されます。 ArrayBuffer



は、多数の配列タイプで表されます: Int8Array



Uint8Array



Uint8ClampedArray



Int16Array



Uint16Array



Int32Array



Uint32Array



Float32Array



Float64Array







その結果、値ではなく参照によってストリーム間でデータが転送されるようになった場合、このデータを2つのストリームから同時に変更できますか? 標準では、このような動作は禁止されています。 postMessage()



, ( «neutered»). postMessage()



-, . JS-.



まとめ



- HTML5 , , .



, -:





-:





現在、Webワーカーは最新のブラウザのほとんどをサポートしています。特に、Chrome、Safari、FireFoxブラウザーは、2009年頃からそれらをサポートしています。WebワーカーはMS Edgeでもサポートされており、IE10以降Internet Explorerでサポートされています。



プロジェクトでWebワーカーを使用する場合、このプロジェクトと特定のブラウザーとの互換性を確認するには、単純な型チェックを実行するだけで十分if (typeof Worker !== "undefined")



です。ブラウザーでWebワーカーがサポートされていないことが判明した場合、提供されていれば、ワーカーが使用されていないコードの代替バージョンに切り替えることができます(このアプローチでは、タイムアウトまたは呼び出しによってコードを実行できますrequestAnimationFrame()



)。



親愛なる読者! ウェブワーカーを使用していますか?






All Articles