📜 ⬆️ ⬇️

JavaScript Web Workers: Secure Concurrency

Web workers provide a programmer with a tool to execute JavaScript code outside the main thread, which is responsible for what happens in the browser. This thread handles requests for data output to the screen, it supports user interaction, perceiving, in particular, keyboard presses and mouse clicks. The same thread is responsible for supporting network interaction, for example, processing AJAX requests.

Event handling and AJAX requests are asynchronous, it can be considered a way of executing some code outside the main thread, however, all the load on performing such operations still falls on the main thread, and, to ensure normal operation of the user interface, these operations need to be performed very fast. Otherwise, the interactive elements of the pages will not work as well as expected.

image
')
Web workers allow you to execute JavaScript code in a separate stream, which is completely independent of the main stream and what usually happens in it.

Recently there has been a lot of talk about what practical problems can be solved with the help of web workers. Given the computational power that even ordinary modern personal computers have, and the fact that mobile devices are approaching them in terms of performance and memory size, now in browser applications you can do a lot of things that used to be considered too complicated.

In the material, the translation that we publish today, we will consider the features of using web workers for solving problems that are too heavy for the main thread. In particular, the discussion here will focus on how to organize data exchange between the main stream and the web worker flow. A couple of examples illustrating various scenarios for using web workers will be considered here.

Basics of working with web workers


Often, application performance is analyzed in an isolated environment, on a developer's computer, and they are satisfied with what they do. With this approach, for example, a minimum of additional programs are running on such a computer. However, in real life is not so. For example, a regular user can run many more applications with our program.

As a result, applications that run normally in an isolated environment, without using separate threads created by web workers, may need such threads in order to look decent in real-world use cases.

Starting a web worker is reduced to creating an appropriate object with passing it the path to a file with JavaScript code.

new Worker('worker-script.js') 

After creation, the worker works in a separate thread, independent of the main thread, executing any code that is transferred to it as a file. The browser, when searching for the file specified when creating the web worker, uses a relative path whose root is the folder that contains the current HTML page.

Data between the workers and the main stream is transmitted through two complementary mechanisms:


The message event handler takes an event argument in the same way as other handlers. This argument has a data property that contains the data passed to the receiving side.

Using the above mechanisms, you can organize bidirectional information exchange. The code in the main thread can use the postMessage() function to send messages to the worker. A worker can send responses to the main thread using the postMessage() implementation, which is globally available in the worker's environment.

Here is how a simple data exchange between the main stream and the web worker looks like. This shows how the code on the HTML page sends a message to the worker and waits for a response:

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

Here is the code of the web worker in which the processing of messages coming from the page and the mechanism for sending responses are organized:

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

After executing this code, the following will be displayed in the console:

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

When using web workers, it is expected that they will be performed for a long time, rather than used for short tasks, constantly starting and stopping. During the life cycle of a worker, multiple messaging sessions with the main thread can take place. Implementing web workers ensures safe, non-conflicting code execution through two mechanisms:


The flow of each worker has a dedicated, isolated global environment that is different from the JavaScript environment in which the code on the HTML page is running. Workers do not have access to the mechanisms accessible from the page environment. They do not have access to the DOM, they cannot work with window and document objects.

The worker has its own versions of some mechanisms, like the console object for logging messages to the developer console, and the XMLHttpRequest object for performing AJAX requests. However, in other matters, it is expected that the code executed by the worker is self-sufficient. So, for example, the data from the worker's thread, which is planned to be used in the main thread, should be transferred as a data object via the postMessage() function.

Moreover, the data passed by the postMessage() function is copied, that is, changes to this data made by the main thread will not affect the original data that is in the worker's thread. This is an internal defense mechanism against conflicting parallel data changes that are transmitted between the main stream and the worker thread.

Options for using web workers


A typical use case for a web worker is any task that can become complex in terms of the amount of computations that can be performed in the main thread. This complexity is expressed either in the consumption of too much CPU resources, or in that this task may require an unpredictably large amount of time required, for example, to access data.

Here are some of the possible uses for web workers:


In the simplest case, choosing a problem to solve with the help of a web worker, you should pay attention to the amount of computation needed to solve it. However, it may be very important to record the time required, for example, to access network resources. Very often, data exchange sessions via the Internet may take very little time, milliseconds, but sometimes network resources may be unavailable, data exchange may stop until the connection is restored or until the request timeout occurs (which can take 1-2 minutes).

And even if it may not take too much time to execute a certain code when testing a program in an isolated development environment, executing the code in real conditions, when, in addition to it, many more tasks are performed on the user's computer, may become a problem.

The following examples show a couple of practical uses for web workers.

Handling collisions in the game


Nowadays, HTML5 games that are executed in browsers are quite common. One of the central gaming mechanisms is the calculation of the movements and interactions of the objects of the game world. In some games, the number of moving elements is relatively small, it is easy to animate them (for example, as in this version of Super Mario ). However, let's assume that we have a game that requires more intensive calculations.

In this example, many multicolored objects are presented (we will consider them balls or balls), which, being in a closed rectangular space, move, bouncing off its walls. Our task is that the balls, firstly, do not leave this space, and secondly - that they bounce off each other. That is, we need to handle their collisions with each other and with the boundaries of the playing field.

Boundary collision handling is a relatively simple task, it doesn’t require serious calculations, but collision detection of objects with each other can require a lot of computational resources, since the complexity of such a task, if you don’t go into details, is proportional to the square of the number of objects. Namely, for n balls, you need to check the position of each of them in comparison with all the others in order to understand if they do not intersect, and do not need them to change the direction of motion, realizing a rebound, which leads to the number of operations equal to n in the square .

So, for 50 balls you need to make about 2500 comparisons. For 100 balls - already 10,000 checks are required (in fact, this number is slightly less than half of the specified, because if a check for a collision of a ball n with a ball m , then it is no longer necessary to check the collision of a ball m with a ball n , however, in spite of this, a large amount of computations is required to solve such a problem).

In this example, the execution of calculations to handle collisions of balls with each other and with the boundaries of the playing field is performed in a separate thread of the web worker. This thread is accessed 60 times per second, which corresponds to the browser animation speed, or to each requestAnimationFrame() call. Here we describe the World object, which contains a list of Ball objects. Each Ball object stores information about its current position and speed (there is also information about the radius and color of the object, which allow you to display it on the screen).

The output of the balls in their current position is performed in the main thread (which has access to the Canvas object and its drawing context). Ball positions are updated in the web worker thread. The speed (in particular, the direction of movement of the balls) changes if they collide with the boundaries of the playing field or with other balls.

The World object is passed between the client code in the browser and the worker thread. This is a relatively small object, even for a few hundred balls (for example, for 100 balls, given that you need about 64 bytes of data per one, the total volume will be about 6400 bytes). As a result, the main problem here is not the transfer of data about game objects, but the computational load on the system.

The full code for this example can be found here . There is, in particular, the Ball class, which is used to represent animated objects, and the World class, which implements the move() and draw() methods, which perform animation.

If we performed the animation without using workers, the main code of this example would look like this:

 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(); 

It uses requestAnimationFrame() to call the animationStep() function 60 times per second, during the screen refresh period. An animation step consists of calling the move() method, which updates the position of each ball (and, possibly, the direction of its movement), and from the call to the draw() method, which outputs, using the objects of the canvas object, balls in new positions.

In order to use a worker's thread in this program, the calculations performed when calling the move() method, that is, the code from World.move() , must be moved to the worker. The World object will be passed, in the form of a data object, to the worker's thread, using the postMessage() call, which will allow you to call the move() method here. Obviously, you need to transfer between the main thread and the web worker at the World object, since it contains a list of Ball objects displayed on the screen and data about the rectangular area within which they should remain. At the same time, Ball objects contain all information about the position, speed, and direction of movement of the respective balls.

After making changes to the project designed to use a web worker, the animation loop will look like this:

 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(); 

Here's what the worker code will look like:

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

The code presented here is based on the fact that the web worker's thread accepts the World object passed to it using postMessage() from the main thread, and then passes the same object back to the main thread, having previously calculated the new values ​​for the position and speed of the game objects. of the world. Remember that the browser makes a copy of this object when it is transmitted between threads. Here we proceed from the assumption that the time required to create a copy of the World object is much less than O (n ** n), that is, the time required for collision detection (in fact, a relatively small amount of data is stored in the World object ).

When launching a new code, however, we will encounter an unexpected error:

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

It turns out that in the process of copying an object when passing it using the postMessage() function, the data of the object's properties is copied, but not its prototype. The methods of the World object are separated from the prototype when the object is copied and passed to the worker. This is part of the structure cloning algorithm , a standard way of copying objects when transferring them between the main thread and the web worker. This process is also known as serialization .

In order to get rid of the above error, we will add to the World class a method for creating its new instance (which will have a prototype with methods) and reassigning the properties of this object based on the data passed using postMessage() :

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

Attempting to execute code after these changes results in another, similar error. The fact is that the list of Ball objects that World stores also needs to be restored:

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

The World class implementation needs to be expanded in order to restore each Ball object based on the data passed to postMessage() , just like the World class itself is being restored.

Now the World class will look like this:

 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; } 

A similar restoreFromData() method is restoreFromData() implemented in the Ball class:

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

Thanks to these changes, the animation is performed correctly, calculations are made for the movements of each of the possibly hundreds of balls in the worker's stream and output them in the browser in new positions at a speed of 60 times per second.

This example of working with web worker threads demonstrates how to solve a problem that requires large computational resources, but not memory. What if we face a task that requires a lot of memory?

Threshold image processing


In this example, we consider an application that places a significant load on both the processor and memory. It takes pixel data from an image represented as an HTML5 canvas object and transforms it, creating another image based on it.

Here we use the image processing library created in 2012 by Ilmari Heikinen. The program will take a color image and convert it into a binary black and white image. During the conversion, a gray threshold value will be used: pixels whose color values ​​in gray are less than this threshold will turn black, pixels with large values ​​will turn white.

The code for obtaining a new image is traversed over all color values ​​(represented in RGB format), and uses a formula to convert them to the appropriate shades of gray, after which it decides whether the final pixel will be black or white:

 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; }; 

Here is the original image.


Source image

Here is what happens after processing.


Processed image

An example code can be found here .

Even when working with small images, the amount of data that needs to be processed, as well as the computational processing costs, can be significant. For example, in an image of 640x480 pixels there are 307200 pixels, each of which corresponds to 4 bytes of RGBA data (A is the alpha channel that sets the color transparency). As a result, the size of this image is approximately 1.2 MB. Our plan is to use a web worker to iterate over pixel data and convert their color values. The pixel data for the image will be transferred from the main stream to the web worker, and the modified image will be returned from the worker to the main stream. It would be nice if it were not necessary to copy this data every time they cross the border between the main stream and the worker stream.

The postMessage() function can be used by setting one or more properties that describe the data that is passed in the message by reference. That is, not copies of the data are transferred, but links to them. It looks like this:

 <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> 

Here you can use any object that implements the Transferable interface. The design of the data.buffer object meets this requirement - it is of type Uint8ClampedArray (arrays of this type are designed to store 8-bit image data). ImageData is what the getImageData() method returns for the HTML5 canvas object context .

The Transferable interface implements several standard data types: ArrayBuffer , MessagePort , and ImageBitmap . ArrayBuffer , in turn, is represented by a number of array types: Int8Array , Uint8Array , Uint8ClampedArray , Int16Array , Uint16Array , Int32Array , Uint32Array , Float32Array , Float64Array .

As a result, if data is now transferred between streams by reference, and not by value, can this data be modified from two streams simultaneously? . postMessage() , ( «neutered»). postMessage() -, . JS-.

Results


- HTML5 , , .

, -:


-:


- . , Chrome, Safari FireFox 2009 . - MS Edge, Internet Explorer IE10.

-, if (typeof Worker !== "undefined") . , - , , , , ( - requestAnimationFrame() ).

Dear readers! -?

Source: https://habr.com/ru/post/352828/


All Articles