jQuery 3 - Promises, Deferred & WebWorkers |
Written by Ian Elliot | |||||||||
Thursday, 07 September 2017 | |||||||||
Page 2 of 2
The Trouble With ThreadsIf you have looked at the problem of writing multi-threaded programs this is where you might be getting worried. Starting a new thread so easily seems to be and easy way to do something dangerous. However Web Workers have been implemented in a way that restricts the way that you use them to make them safe. At first these restrictions might seem tough to live with but after a while you realize that they are perfectly reasonable and don't really stop you from doing anything. The main simplification about threading with Web Workers is that the the new thread cannot share anything with the UI thread. The new thread cannot access any objects that the UI Thread can. This means it cannot interact with any global objects and it cannot interact with the DOM or the user interface in any way. The new thread runs in a little world of its own - but don't panic it can communicate with the UI thread in a very simple way. This inability to share objects may seem a little restrictive but it is a restriction that is necessary to make sure that the two threads you now have don't try to access the same object at the same time. If this was allowed to happen you would need to introduce a lot of complicated machinery - locks, semaphores and so on - to make sure that the access was orderly and didn't give rise to any very difficult to find bugs - race conditions, deadlock and so on. in other words the Web Worker has big restrictions so that you can use it without complication and without any danger. For most purposes it is sufficient and hence very effective. Web Workers do have access to all of the global core JavaScript objects that you might expect. They can also access some functions that are normally associated with the DOM - XMLHttpRequest() and setInterval etc. The rule is that if the object could be shared in anyway with the UI thread then you cannot get access to it and this is a condition that is obviously satisfied for all of the core JavaScript objects and the DOM objects that are allowed. To make up for this restriction there are two new objects that the Web Worker can access - WorkerNavigator and WorkerLocation. The navigator provides basic information about the app's context - the browser in use, appName, appVersion and so on. The location object provides details of where the app is in terms of the current URL if these two objects don't provide enough information you can easily arrange to pass the worker thread additional data of your choosing. Basic Communication MethodsSo if the Web Worker is isolated from the UI thread how do the two communicate? The answer is that they both use events and a method that causes events on the other thread. UI Thread To Worker ThreadLet's start with the UI thread sending a message to the worker thread. The Worker object that is created on the UI thread has a postMessage method which triggers a message event on the worker thread. Notice that this is where the thread crossover occurs. The Worker object is running on the UI thread but the even occurs in the code running on the worker thread. For example:
The postMessage method triggers a message event in the worker code and sends it an event object that includes the data packaged as an object or an array. To get the message sent to the Worker object you have to set up and event handler and retrieve the event object's data property. For example, in the Web Worker code you would write:
In the Web Worker code the global context is provided by this or self and this gives access to all of the methods and objects documented. To get the message you would use:
Of course as you are passing an object to the event handler you could package as many data items as you needed to. Notice that it is important to be very clear what is going on here. The postMessage method call is on the UI thread but the event handler is on the worker thread. It is also important to realize that the data that is passed between the two threads isn't shared. A copy is made using the structured clone algorithm and it is this copy that the worker received. You can use a wide range of types of data to pass to the worker but if it is big the time taken to copy could be significant. In which case you need to use a transferable object which is shared rather than copied. Worker Thread To UI ThreadPassing data from the worker thread to the UI thread works in exactly the same way - only the other way round. You use the postMessage method in the worker thread and attach an event handler for the message event in the UI thread. For example, in the worker code: this. notice that in this case you can use this or self to call postMesage because you are running inside the web worker. This triggers a message event in the UI thread and you can define a handler and retrieve the data using
Once again you have to be very clear that you understand what is running where. In this case the postMessage method is running on the worker thread and the event handler is running on the UI thread. This is about all there is to using Web Workers. There are some details about error handling and terminating the thread before it is complete but these are details. The general idea is that you use the message event to communicate between the two threads. There is one subtle point that is worth keeping in mind. The events that you trigger in passing data between the two threads will happen in the order that you trigger them but they may not be handled promptly. For example, if you start your worker thread doing an intensive calculation then triggering a "how are you doing" message event from the UI thread might not work as you expect. It could be that the worker thread is so occupied with its task that events are ignored until it reaches the end and this is not what you might expect. The same happens with the messages passed from the worker thread but in this case the UI thread is generally not so focused on one task and so events usually get processed. in general events going from the worker thread to the UI get processed as part of keeping the UI responsive. Events going the other way, i.e. from the UI thread to the worker, are not so reliable. A Simple Web Worker ExampleAs example let's use the calculation of Pi example again. First we need a new JavaScript file called pi.js containing the following code:
You can see that this worker is just a simple modification to the earlier program. The most important change is that we now no longer need to break the calculation up into chunks. As it is being performed on a separate thread it can run to completion and take as much time at it likes without any fear of blocking the UI - which is running on a different thread. When the calculation is complete it uses the postMessage method to fire a "message" event on the UI thread and supply the result. The UI thread code is simply:
Where we are using the UI of the previous example. Notice that we can use jQuery to work with the event handlers. The only small problem is that jQuery doesn't transfer the data field from the original Event object to its own and so we have to access it via originalEvent. When the button is clicked the Worker constructor loads pi.js and starts it running on a new thread. The constructor returns a Worker object which runs on the UI thread in the variable worker. When the worker thread is finished it fires the "message" event which is handled by the anonymous function which displays the result. This is a very typical use of worker threads. The UI thread generally sends some data to initialize the worker thread and then simply waits for message events. Worker With PromisesOur next task is to make changes so that the asynchronous aspect of the worker is handled by a Promise object. All of the new code is in the UI thread. You can run jQuery on the worker thread but it isn't an easy task because jQuery expects to access the DOM and this isn't accessible from the worker thread. We need to package the interaction between the UI thread and the worker as a function:
The first thing we need to do is create the Deferred object that eventually will be used to return a Promise object and to change the state of the Promise when everything is finished.
The next thing we need to do is start the worker off:
Now we have a worker thread computing the result over some length of time and what we have to do to make sure that the UI is free to get on with its job is to bring this function to an end and return the promise object. However this doesn't solve the problem of setting the promise object to resolved. How can we do this so that the UI thread isn't blocked? The answer is exactly as we have done it before - we set up an event handler for the message event:
Finally we return the promise object.
The complete code for the function is:
So how do we use the new function and the promise object it returns? The answer is, as before, that you use it just like any other asynchronous function that returns a Promise.
The button's event handler now just has a simple call to piWorker function and this immediately returns a promise object The then method of the promise object is used to define what should happen when the thread completes and returns the "promised" value. That is the function passed to then sets the button caption to the value and re-enables the button. This is very much easier to use as the fact that we are using a separate thread is completely hidden from the client. In addition the client is able to define what is to be done with the result without having to delve into the inner workings of the function. Where Next?Wrapping worker threads in a promise makes it easy for clients to make use of real asynchronous functions in JavaScript. While it might be difficult to use jQuery in the worker thread this is generally no disadvantage. When it comes to adding promises all of the work has to be done on the UI thread. You can add error handling and periodic progress reports using the usual promise methods. You can also pass parameters to the worker thread to control it. Unusually you can even arrange to cancel the asynchronous operation because there is a terminate method that will dispose of a worker thread. However as with all things bringing a worker thread to a halt is usually more of a problem than you might imagine. There are also lots of additional features of the worker thread that you might want to find out about including shared worker threads and transferable objects. Summary
More Information
{CONTENTSJQueryAsync}
To be informed about new articles on I Programmer, sign up for our weekly newsletter, subscribe to the RSS feed and follow us on Twitter, Facebook or Linkedin.
Comments
or email your comment to: comments@i-programmer.info
|
|||||||||
Last Updated ( Thursday, 05 May 2022 ) |