Web Worker

Overview

The JavaScript language uses a single-threaded model, that is, all tasks can only be completed on one thread, and only one thing can be done at a time. The previous tasks have not been completed, and the following tasks can only be waited for. With the increase in computing power of computers, especially the emergence of multi-core CPUs, single threading brings great inconvenience and cannot fully utilize the computing power of computers.

The role of Web Worker is to create a multi-threaded environment for JavaScript, allowing the main thread to create Worker threads and assign some tasks to the latter to run. While the main thread is running, the Worker thread runs in the background, and the two do not interfere with each other. Wait until the Worker thread completes the calculation task, and then return the result to the main thread. The advantage of this is that some computationally intensive or high-latency tasks can be handed over to the Worker thread for execution, and the main thread (usually responsible for UI interaction) can remain smooth without being blocked or slowed down.

Once the Worker thread is successfully created, it will always run and will not be interrupted by activities on the main thread (such as the user clicking a button or submitting a form). This is conducive to responding to the communication of the main thread at any time. However, this also causes the Worker to consume more resources and should not be overused, and once it is used up, it should be closed.

Web Worker has the following points of attention.

(1) Same origin restriction

The script file assigned to the Worker thread to run must be of the same source as the script file of the main thread.

(2) DOM restrictions

The global object where the Worker thread is located is different from the main thread. It cannot read the DOM objects of the webpage where the main thread is located, nor can it use objects such as document, window, and parent. However, the Worker thread can use the navigator object and the location object.

(3) Global object restrictions

Worker's global object WorkerGlobalScope is different from the web page's global object Window, and many interfaces are not available. For example, in theory, the Worker thread cannot use the console.log, because the standard does not mention that the Worker's global object has the console interface, only the Navigator interface and the Location interface are defined. However, the browser actually supports the Worker thread to use the console.log, and it is safe to not use this method.

(4) Communications

Worker thread and main thread are not in the same context, they cannot communicate directly, and must be done through messages.

(5) Script restrictions

The Worker thread cannot execute the alert() method and the confirm() method, but can use the XMLHttpRequest object to make AJAX requests.

(6) File restrictions

The Worker thread cannot read local files, that is, it cannot open the local file system (file://). The script it loads must come from the network.

Basic usage

Main thread

The main thread uses the new command to call the Worker() constructor to create a new Worker thread.

var worker = new Worker("work.js");

The parameter of the Worker() constructor is a script file, which is the task to be performed by the Worker thread. Since Worker cannot read local files, this script must come from the network. If the download is not successful (such as a 404 error), the Worker will silently fail.

Then, the main thread calls the worker.postMessage() method to send a message to the Worker.

worker.postMessage("Hello World");
worker.postMessage({ method: "echo", args: ["Work"] });

The parameter of the worker.postMessage() method is the data passed to the worker by the main thread. It can be of various data types, including binary data.

Then, the main thread uses worker.onmessage to specify the monitoring function to receive the messages sent back by the child threads.

worker.onmessage = function (event) {
  doSomething(event.data);
};

function doSomething() {
  // Perform tasks
  worker.postMessage("Work done!");
}

In the above code, the data property of the event object can get the data sent by the Worker.

After the Worker completes the task, the main thread can turn it off.

worker.terminate();

Worker thread

There needs to be a listener function inside the Worker thread to listen for the message event.

self.addEventListener(
  "message",
  function (e) {
    self.postMessage("You said: " + e.data);
  },
  false
);

In the above code, self represents the child thread itself, that is, the global object of the child thread. Therefore, it is equivalent to the following two writing methods.

// Writing method one
this.addEventListener(
  "message",
  function (e) {
    this.postMessage("You said: " + e.data);
  },
  false
);

// Writing method two
addEventListener(
  "message",
  function (e) {
    postMessage("You said: " + e.data);
  },
  false
);

In addition to using self.addEventListener() to specify the listener function, you can also use self.onmessage to specify. The parameter of the listener function is an event object, and its data attribute contains the data sent by the main thread. The self.postMessage() method is used to send a message to the main thread.

According to the data sent by the main thread, the Worker thread can call different methods. The following is an example.

self.addEventListener(
  "message",
  function (e) {
    var data = e.data;
    switch (data.cmd) {
      case "start":
        self.postMessage("WORKER STARTED: " + data.msg);
        break;
      case "stop":
        self.postMessage("WORKER STOPPED: " + data.msg);
        self.close(); // Terminates the worker.
        break;
      default:
        self.postMessage("Unknown command: " + data.msg);
    }
  },
  false
);

In the above code, self.close() is used to close itself inside the Worker.

Worker loading script

If you want to load other scripts inside the Worker, there is a special method importScripts().

importScripts("script1.js");

This method can load multiple scripts at the same time.

importScripts("script1.js", "script2.js");

Error handling

The main thread can monitor the Worker for errors. If an error occurs, the Worker will trigger the error event of the main thread.

worker.onerror = function (event) {
  console.log(
    "ERROR: Line",
    event.lineno,
    "in",
    event.filename,
    ":",
    event.message
  );
};

// or
worker.addEventListener("error", function (event) {
  // ...
});

You can also listen to the error event inside the Worker.

Close Worker

After use, in order to save system resources, the Worker must be closed.

// main thread
worker.terminate();

// Worker thread
self.close();

data communication

As mentioned earlier, the communication content between the main thread and the Worker can be text or objects. It should be noted that this communication is a copy relationship, that is, transfer by value instead of address, and the modification of the communication content by Worker will not affect the main thread. In fact, the internal operating mechanism of the browser is to serialize the communication content first, and then send the serialized string to the Worker, who then restores it.

Binary data can also be exchanged between the main thread and Worker, such as File, Blob, ArrayBuffer and other types, and can also be sent between threads. Below is an example.

// main thread
var uInt8Array = new Uint8Array(new ArrayBuffer(10));
for (var i = 0; i < uInt8Array.length; ++i) {
  uInt8Array[i] = i * 2; // [0, 2, 4, 6, 8,...]
}
worker.postMessage(uInt8Array);

// Worker thread
self.onmessage = function (e) {
  var uInt8Array = e.data;
  postMessage(
    "Inside worker.js: uInt8Array.toString() = " + uInt8Array.toString()
  );
  postMessage(
    "Inside worker.js: uInt8Array.byteLength = " + uInt8Array.byteLength
  );
};

However, sending binary data in copy mode will cause performance problems. For example, if the main thread sends a 500MB file to Worker, the browser will generate a copy of the original file by default. To solve this problem, JavaScript allows the main thread to transfer binary data directly to the child threads, but once transferred, the main thread can no longer use the binary data. This is to prevent the troublesome situation of multiple threads modifying data at the same time. This method of transferring data is called Transferable Objects. This allows the main thread to quickly hand over the data to the Worker, which is very convenient for image processing, sound processing, 3D calculations, etc., without any performance burden.

If you want to directly transfer control of the data, you must use the following wording.

// Transferable Objects format
worker.postMessage(arrayBuffer, [arrayBuffer]);

// example
var ab = new ArrayBuffer(1);
worker.postMessage(ab, [ab]);

Web Worker on the same page

Normally, Worker loads a separate JavaScript script file, but it can also load code on the same web page as the main thread.

<!DOCTYPE html>
  <body>
    <script id="worker" type="app/worker">
      addEventListener('message', function () {
        postMessage('some message');
      }, false);
    </script>
  </body>
</html>

The above is a script embedded in a web page. Note that the type attribute of the <script> tag must be specified as a value that the browser does not recognize. The example above is app/worker.

Then, read the script embedded in the page and process it with Worker.

var blob = new Blob([document.querySelector("#worker").textContent]);
var url = window.URL.createObjectURL(blob);
var worker = new Worker(url);

worker.onmessage = function (e) {
  // e.data ==='some message'
};

In the above code, first convert the script code embedded in the web page into a binary object, then generate a URL for the binary object, and then let the Worker load the URL. In this way, the main thread and Worker code are on the same web page.

Example: Worker thread completes polling

Sometimes, the browser needs to poll the server status in order to know the status change as soon as possible. This job can be placed in Worker.

function createWorker(f) {
  var blob = new Blob(['(' + f.toString() +')()']);
  var url = window.URL.createObjectURL(blob);
  var worker = new Worker(url);
  return worker;
}

var pollingWorker = createWorker(function (e) {
  var cache;

  function compare(new, old) {... };

  setInterval(function () {
    fetch('/my-api-endpoint').then(function (res) {
      var data = res.json();

      if (!compare(data, cache)) {
        cache = data;
        self.postMessage(data);
      }
    })
  }, 1000)
});

pollingWorker.onmessage = function () {
  // render data
}

pollingWorker.postMessage('init');

In the above code, Worker polls the data every second and compares it with the cache. If they are inconsistent, it means that the server has new changes, so the main thread must be notified.

Example: Worker New Worker

Worker threads can be created inside Worker threads (currently only supported by Firefox browser). The following example is to assign a computationally intensive task to 10 Workers.

The main thread code is as follows.

var worker = new Worker("worker.js");
worker.onmessage = function (event) {
  document.getElementById("result").textContent = event.data;
};

Worker thread code is as follows.

// worker.js

// settings
var num_workers = 10;
var items_per_worker = 1000000;

// start the workers
var result = 0;
var pending_workers = num_workers;
for (var i = 0; i < num_workers; i += 1) {
  var worker = new Worker("core.js");
  worker.postMessage(i * items_per_worker);
  worker.postMessage((i + 1) * items_per_worker);
  worker.onmessage = storeResult;
}

// handle the results
function storeResult(event) {
  result += event.data;
  pending_workers -= 1;
  if (pending_workers <= 0) postMessage(result); // finished!
}

In the above code, 10 Worker threads are newly created inside the Worker thread, and messages are sent to these 10 Workers in turn to inform the starting point and end point of the calculation. The code of the calculation task script is as follows.

// core.js
var start;
onmessage = getStart;
function getStart(event) {
  start = event.data;
  onmessage = getEnd;
}

var end;
function getEnd(event) {
  end = event.data;
  onmessage = null;
  work();
}

function work() {
  var result = 0;
  for (var i = start; i < end; i += 1) {
    // perform some complex calculation here
    result += 1;
  }
  postMessage(result);
  close();
}

API

Main thread

The browser natively provides the Worker() constructor for the main thread to generate Worker threads.

var myWorker = new Worker(jsUrl, options);

The Worker() constructor can accept two parameters. The first parameter is the URL of the script (must comply with the same-origin policy). This parameter is required, and only JS scripts can be loaded, otherwise an error will be reported. The second parameter is the configuration object, which is optional. One of its functions is to specify the name of the Worker, which is used to distinguish multiple Worker threads.

// main thread
var myWorker = new Worker("worker.js", { name: "myWorker" });

// Worker thread
self.name; // myWorker

The Worker() constructor returns a Worker thread object for the main thread to operate Worker. The attributes and methods of the Worker thread object are as follows.

-Worker.onerror: Specify the listener function for the error event. -Worker.onmessage: Specify the listener function for the message event, and the sent data is in the Event.data property. -Worker.onmessageerror: Specify the listener function for the messageerror event. This event is triggered when the sent data cannot be serialized into a string. -Worker.postMessage(): Send a message to the Worker thread. -Worker.terminate(): Terminate the Worker thread immediately.

Worker thread

Web Worker has its own global object, not the window of the main thread, but a global object customized specifically for Worker. Therefore, not all objects and methods defined on window can be used.

The Worker thread has some of its own global properties and methods.

-self.name: Worker's name. This attribute is read-only and is specified by the constructor. -self.onmessage: Specify the listener function for the message event. -self.onmessageerror: Specify the listener function for the messageerror event. This event is triggered when the sent data cannot be serialized into a string. -self.close(): Close the Worker thread. -self.postMessage(): Send a message to the thread that generated this Worker. -self.importScripts(): Load JS scripts.

(Finish)