Saturday, 12 August 2017

Multi-Threading with JavaScript Web Workers

Web Workers allow developers to create background threads which enables UI to run smoothly. This is useful for scenarios where the logical part of code is CPU intensive and it might slow down the UI.

Although the Web Workers can run code containing a wide variety of logic there are some restrictions that apply:
  • The DOM cannot be manipulated from inside the Web Worker.
  • The access to global variables and Objects(functions) from parent is restricted.
  • Limited access to window object. Read-only for window.location property.
  • No access tdocument and parent  object.
  • Newly spawned Web Workers can only be under the same origin as the parent.


Fig 1.0  Web Worker Types

There 5 Web Worker types that are laid out in Fig 1.0. But 'ChromeWorker' is specifc to only FireFox. The browser compatibility list can be found at http://caniuse.com/#feat=webworkers .

Although each Web Worker type has a crucial role to play in an application only Dedicated and Shared Workers have been discussed in this blog post.

Dedicated Worker:

Dedicated Web Workers cannot be shared by other scripts so they are only accessible by the script that created them. In this blog post a worker will be created to generate Fibonacci  sequence. Also the methods provided by the Worker object will be discussed. 

Spawn:

let fibWorker;

if(window.Worker){

    fibWorker = new Worker('fibonnaci.js');

}

A new Worker object can be spawned by calling the  new Worker(<file URI>) constructor. The <file URI> is the location of the file relative to the origin. 

Messaging:



Fig 2.0 Communication between UI and Worker threads

The code run by the Worker cannot access functions from the caller script. So it has to communicate via messages. 

//main.js file
let listFibonacciNumbers = (n = 0) => {
   fibWorker.postMessage({'number': n});
}

fibWorker.onmessage = (e) =>{
     console.log('Fibonacci numbers:\n');
     console.log(e.data.numbers);
}

//------------------------------------------------------------------------------//

let generateFibonacciNumbers = (size) => {
   let n = size;
   let arr = [];

   for(let i =0; i<n ; i++){
     arr[i] = i<2?1: arr[i-2]+arr[i-1];
   }
    
   return arr;
}
//fibonacci.js onmessage = (e)=>{ let numbers = generateFibonacciNumbers(e.data.number); postMessage({'numbers':numbers}); }

The sample code above has two sections representing main.js and fiboanacci.js files. In each section there are methods that help the code communicate between two files.

In main.js file the listFibonacciNumbers method is used to send the Worker a message to start generating the numbers of size n . When the fibonacci number generation is completed the fiboanacci.js file sends back the array and fibWorker.onmessage callback will receive the array in main.js file. And any type of data can be sent back and forth between fiboanacci.js and main.js files using postMessage  method. This bidirectional messaging helps the UI thread to sync with the Worker thread.

Termination:

Terminating a worker form the main thread is rather simple. When the worker is terminated all its operations will cease and it will no longer be active.

fibWorker.terminate();

A worker can terminate itself by calling the following method from inside the worker code:

close();


Shared Worker :

In contrast to dedicated worker the shared worker can be accessed from any script which has the same origin as the worker script. This allows multiple windows, iframes and other workers to access it.
Fig 3.0 Communication between UI and Shared Worker threads

//main1.js
let inputNum = document.getElementById('fibnum');

if (!!window.SharedWorker) {
  let fibWorker = new SharedWorker("worker.js");

  inputNum.onchange = function() {
    fibWorker.port.postMessage({'size': inputNum.value});
  }

  fibWorker.port.onmessage = function(e) {
     console.log(e.data.fibArray);
  }
}


//main2.js
let inputNum = document.getElementById('fibnum');

if (!!window.SharedWorker) {
  let fibWorker = new SharedWorker("worker.js");

  inputNum.onchange = function() {
    fibWorker.port.postMessage({'size': inputNum.value});
  }

  fibWorker.port.onmessage = function(e) {
     console.log(e.data.fibArray);
  }
}

The sample codes above belong to two different files which are  main1.js  and  main2.js . In this case the codes for the two files perform the same function of requesting fibonacci sequence array. But  in general there can be many files that may ask a single worker to perform different tasks. UI thread can only communicate with the ServiceWorker  via specific port. In  main1.js and main2.js files the fibWorker.port.onmessage implicitly creates the port for communication. Using fibWorker.port.postMessage the main1.js and main2.js  files can communicate with the worker. When the result is received from the worker the fibWorker.port.onmessage gets the generated fibonacci array.

//workers.js
let generateFibonacci = (size) => {
   let n = size;
   let arr = [];

   for(let i =0; i<n ; i++){
     arr[i] = i<2?1: arr[i-2]+arr[i-1];
   }
    
   return arr;
}

onconnect (e) => {
  let port = e.ports[0];

  port.onmessage = function(e) {
    let fibArray = generateFibonacci(e.data.size);
        
    port.postMessage({arry: fibArray});
  }

}

The code above belongs to the worker.js file. It has a method called generateFibonacci which takes in  n as the argument to generate Fibonacci sequence of  that size. The onconnect method automatically fires as soon as the worker is created. Then using port.onmessage event it can receive the desired size of fibArray so it can send back the generated array using port.postMessage method.

No comments:

Post a Comment