ARTICLE
Threading with Web Workers
From WebAssembly in Action by C. Gerard Gallant
This article covers:
§ Using a web worker to fetch and compile a WebAssembly module
§ Instantiating a WebAssembly module on behalf of Emscripten’s JavaScript code
__________________________________________________________________
Take 37% off WebAssembly in Action. Just enter fccgallant into the discount code box at checkout at manning.com.
__________________________________________________________________
In this article, you’re going to learn about the different options for using threads in a browser with relation to WebAssembly modules.
INFO: A thread is a path of execution within a process, and a process can have multiple threads. A pthread, also known as a POSIX thread, is an API defined by the POSIX.1c standard for an execution module which is independent of programming language: https://en.wikipedia.org/wiki/POSIX_Threads. For more on pthreads, you’re going to have to pick up a copy of the book.
By default, both the UI and JavaScript of a webpage operate in a single thread. If your code does too much processing without yielding to the UI periodically, it can cause the UI to become unresponsive. Your animations freeze and the controls on the webpage won’t respond to a user’s input which can be frustrating for a user.
If the webpage remains unresponsive for long enough (typically ten seconds), a browser may prompt the user to see if they want to stop the page, as shown in figure 1. If the user stops the script on your webpage, your webpage may no longer function as expected unless the user refreshes it.
TIP: to keep webpages as responsive as possible, whenever you interact with a Web API that has both synchronous and asynchronous functions, it’s a best practice to use the asynchronous functions.
Sometimes doing some heavy processing without interfering with the UI is desired, and browser makers came up with something called web workers.
Benefits of web workers
What do web workers do and why would you want to use them? They bring the ability to create background threads to browsers. As shown in figure 2, web workers allow you to run JavaScript in a thread which is separate from the UI thread, and communication between the two is accomplished by passing messages.
Unlike the UI thread, it’s permitted to use synchronous functions in a web worker, if desired, because doing this doesn’t block the UI thread. Within a worker, you can spawn additional workers and you have access to many of the same items that you have access to in the UI thread like fetch, WebSockets, and IndexedDB. For a complete list of APIs available to web workers, you can visit the following webpage: https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API/Functions_and_classes_available_to_workers
Another advantage of web workers is that most devices now have multiple cores. If you’re able to split up your processing across several threads, the length of time it takes to complete the processing should decrease. Web workers are also supported in nearly all web browsers, including mobile browsers.
WebAssembly modules can make use of web workers in several ways:
- A web worker can be used to pre-fetch a WebAssembly module. The web worker can download and compile a WebAssembly module and then pass that compiled module to the main thread. The main thread can then instantiate the compiled module and use it as per normal.
- Emscripten supports the ability to generate two WebAssembly modules, where one sits in the main thread and the other in a web worker. The two modules communicate by using Emscripten helper functions defined in Emscripten’s
Worker API
. You won’t learn about this approach in this article but you’ll see the JavaScript versions of many of Emscripten’s functions. For more information about Emscripten’s Worker API, you can visit the following webpage: https://emscripten.org/docs/api_reference/emscripten.h.html?highlight=build_as_worker#worker-api
Worker API You need to create two C or C++ files in order to compile one to run in the main thread and one to run in the web worker. The web worker file needs to be compiled with the -s BUILD_AS_WORKER=1
flag set.
- A post-MVP feature is being developed which creates a special kind of web worker that allows a WebAssembly module to use pthreads (POSIX threads). At the moment this approach is still considered experimental and flags need to be enabled in the browser to allow the code to run.
Considerations for using web workers
You’ll learn to use web workers shortly, but you should be aware of the following:
- Web workers have a high startup cost and a high memory cost, and they’re not intended for use in large numbers because they’re expected to be long-lived.
- Because web workers run in a background thread, you have no direct access to the UI features of the webpage or the DOM.
- The only way to communicate with a web worker is by sending postMessage calls and responding to messages via an onmessage event handler.
- Even though the background thread’s processing won’t block the UI thread, you need to be mindful of needless processing and memory usage because you’re still using some of the device’s resources. As an analogy, if a user’s using a lot of apps at once, a lot network requests can use up their phone’s data plan and a lot of processing can use up the phone’s battery.
- Web workers are available only in browsers at the moment. If your WebAssembly module needs to also support Node.js, for example, this is something you’ll need to keep in mind. As of version 10.5, Node.js has experimental support for Worker Threads but they’re unsupported by Emscripten. More information about Node.js Worker Thread support can be found here: https://nodejs.org/api/worker_threads.html
Pre-fetch a WebAssembly module using a web worker
Suppose you have a webpage that needs a WebAssembly module after the page has loaded. Rather than download and instantiate the module as the webpage is loading, you decide to defer the download until after the page has loaded to keep the page load time as fast as possible. To keep your webpage as responsive as possible, you also decide to use a web worker to handle downloading and compiling the WebAssembly module on a background thread.
As illustrated in figure 3, in this section you’ll learn how to do the following:
- Create a web worker
- Download and compile the WebAssembly module while in a web worker
- Pass and receive messages between the main UI thread and worker
- Override Emscripten’s default behavior, where it usually handles downloading and instantiating a WebAssembly module, and use the module which is already compiled
The following steps enumerate the solution for this scenario (figure 4):
- Adjust the calculate primes logic to determine how long it takes the calculations to complete.
- Use Emscripten to generate the WebAssembly files from the calculate primes logic.
- Copy the generated WebAssembly files to the server for use by the browser
- Create the HTML and JavaScript for a webpage that creates a web worker and have Emscripten’s JavaScript use the compiled WebAssembly module received from the worker
- Create the web worker’s JavaScript file which downloads and compiles the WebAssembly module
The first step, shown in figure 5, is to adjust the calculate primes logic to determine how long it takes to do the calculations:
Adjusting the calculate primes logic
Let’s get started. In your WebAssembly\
folder create a Chapter 9\9.3 pre-fetch\source\
folder.
Copy the calculate_primes.cpp
file from your Chapter 7\7.2.2 dlopen\source\
folder to your newly created source\
folder.
Open the calculate_primes.cpp
file with your favorite editor.
For this scenario, you’ll be using a vector
class which is defined in the vector
header to hold the list of prime numbers found within the range specified. You’ll also use the high_resolution_clock
class, defined in the chrono
header, to time how long it takes for your code to determine the prime numbers.
Add the includes for the vector
and chrono
headers following the cstdio
header in the calculate_primes.cpp file as shown in the following code snippet:
#include <vector>
#include <chrono>
Now remove the EMSCRIPTEN_KEEPALIVE
declaration from above the FindPrimes
function, because this function won’t be called from outside the module.
Rather than call printf for every prime number as it’s found, you’re going to modify the logic in the FindPrimes function to add the prime number to a vector
object instead. This is done to determine the execution duration of the calculations themselves without the delay due to a call to the JavaScript code on every loop. The main function is then adjusted to handle sending the prime number information to the browser’s console window.
VECTOR: A vector object is a sequence container for dynamic sized arrays where the storage is automatically increased or decreased as needed. More information on the vector object can be found here: https://en.cppreference.com/w/cpp/container/vector
The modifications that you’ll make to the FindPrimes function are the following:
- Add a parameter to the function which accepts a
std::vector<int>
reference - Remove all of the
printf
calls - Within the
IsPrime
if statement, add the value ini
to the vector pointer
Adjust the FindPrimes function, in your calculate_primes.cpp file, to match the code in the following snippet:
void FindPrimes(int start, int end,
std::vector<int>& primes_found) { #A
for (int i = start; i <= end; i += 2) {
if (IsPrime(i)) {
primes_found.push_back(i); #B
}
}
}
#A A vector pointer parameter has been added
#B The prime number is added to the list
Your next step is to modify the main
function to:
- Update the browser’s console window with the range of numbers which is checked for prime numbers
- Determine how long the FindPrimes function takes to execute by getting the current value of the clock before and after the call to the FindPrimes function and subtracting the difference.
- Create a vector object to hold the prime numbers found and pass it to the FindPrimes function
- Update the browser’s console to indicate how long it took for the FindPrimes function to execute
- Output each of the prime numbers which were found by looping through the vector object’s values
Your main function in your calculate_primes.cpp file should now look like the code in listing 1.
Listing 1 The main function in calculate_primes.cpp
…
int main() {
int start = 3, end = 1000000;
printf("Prime numbers between %d and %d:\n", start, end);
std::chrono::high_resolution_clock::time_point duration_start =
std::chrono::high_resolution_clock::now(); #A
std::vector<int> primes_found;
FindPrimes(start, end, primes_found); #B
std::chrono::high_resolution_clock::time_point duration_end =
std::chrono::high_resolution_clock::now(); #C
std::chrono::duration<double, std::milli> duration =
(duration_end - duration_start); #D
printf("FindPrimes took %f milliseconds to execute\n", duration.count());
printf("The values found:\n");
for(int n : primes_found) { #E
printf("%d ", n);
}
printf("\n");
return 0;
}
#A Get the current time to mark the start of the FindPrimes execution
#B Create a vector object which holds integers and passes it to the FindPrimes function
#C Get the current time to mark the end of the FindPrimes execution
#D Determine the amount of time, in milliseconds, it took FindPrimes to execute
#E Loop through each value in the vector object and output the value to the console
Now that the calculate_primes.cpp
file has been modified, the second step (figure 6) is where you’ll have Emscripten generate the WebAssembly files.
That’s all for now. If you want to learn more about the book, check it out on liveBook here and see this slide deck.