Research on Web worker multithreading API in JavaScript

  • 2020-03-30 04:32:16
  • OfStack

The Web Worker interface is very inconvenient to use. It basically comes with a sandbox, runs a separate js file in the sandbox, and communicates with the main thread through postMessage and onMessge:


var worker = new Worker("my.js");
var bundle = {message:'Hello world', id:1};
worker.postMessage(bundle); //PostMessage can pass a serializable object
worker.onmessage = function(evt){
    console.log(evt.data);    //Compare the object returned from the worker with the object
in the main thread     console.log(bundle);  //{message:'Hello world', id:1}
}


//in my.js
onmessage = function(evt){
    var data = evt.data;
    data.id++;
    postMessage(data); //{message:'Hello world', id:2}
}

The result can be found that the id of the data obtained in the thread is increased, but the id in the main thread bundle is not changed after it is returned. Therefore, the object passed in the thread is actually copied. In this way, the thread does not share the data and thus avoids read and write conflicts, so it is safe. The cost of thread safety is the ability to manipulate the main thread object in a thread.


var worker = new ThreadWorker(bundle ); worker.run(function(bundle){
    //do sth in worker thread...
    this.runOnUiThread(function(bundle ){
        //do sth in main ui thread...
    });
    //...
});

In this code, after we start a worker, we can make any code run in the worker, and when we need to operate the UI thread (such as reading and writing dom), we can return to the main thread for execution through this.runonuithread.

So how do you implement this mechanism? Look at the following code:


function WorkerThread(sharedObj){
    this._worker = new Worker("thread.js");
    this._completes = {};
    this._task_id = 0;
    this.sharedObj = sharedObj;     var self = this;
    this._worker.onmessage = function(evt){
        var ret = evt.data;
        if(ret.__UI_TASK__){
            //run on ui task
            var fn = (new Function("return "+ret.__UI_TASK__))();
            fn(ret.sharedObj);
        }else{
            self.sharedObj = ret.sharedObj;
            self._completes[ret.taskId](ret);
        }
    }
} WorkerThread.prototype.run = function(task, complete){
    var _task = {__THREAD_TASK__:task.toString(), sharedObj: this.sharedObj, taskId: this._task_id};
    this._completes[this._task_id++] = complete;
    this._worker.postMessage(_task);
}

The code above defines a ThreadWorker object that creates a Web Worker running thread.js, saves the Shared object SharedObj, and handles the messages sent back from thread.js.

If a UI_TASK message is returned from thread.js, then run the function from the message, otherwise execute the complete callback to run. Let's see how thread.js writes:


onmessage = function(evt){
    var data = evt.data;     if(data && data.__THREAD_TASK__){
        var task = data.__THREAD_TASK__;
        try{
            var fn = (new Function("return "+task))();             var ctx = {
                threadSignal: true,
                sleep: function(interval){
                    ctx.threadSignal = false;
                    setTimeout(_run, interval);
                },
                runOnUiThread: function(task){
                    postMessage({__UI_TASK__:task.toString(), sharedObj:data.sharedObj});
                }
            }             function _run(){
                ctx.threadSignal = true;
                var ret = fn.call(ctx, data.sharedObj);
                postMessage({error:null, returnValue:ret, __THREAD_TASK__:task, sharedObj:data.sharedObj, taskId: data.taskId});
            }             _run(0);         }catch(ex){
            postMessage({error:ex.toString() , returnValue:null, sharedObj: data.sharedObj});
        }
    }
}

As you can see, the thread. Js to get their message, receiving the UI thread is one of the most important THREAD_TASK, this is the need of the UI thread over worker thread of execution "task", because the function is not serialized, so transfer is a string, worker thread by parsing string into the function to carry out the tasks of the main thread to submit (note that Shared objects in the task sharedObj incoming), after completion of execution returns the results through the message to the UI thread. Let's take a closer look. In addition to the returnValue returnValue, the Shared object sharedObj will also be returned. When returned, since the worker thread and UI thread do not share the object, we manually synchronize the objects on both sides by assigning (is this thread-safe? Why?)

As you can see, the entire process is not complicated, so once implemented, the ThreadWorker can be used in one of two ways:


var t1 = new WorkerThread({i: 100} );         setInterval(function(){
            t1.run(function(sharedObj){
                    return sharedObj.i++;
                },
                function(r){
                    console.log("t1>" + r.returnValue + ":" + r.error);
                }
            );
        }, 500);
var t2 = new WorkerThread({i: 50});         t2.run(function(sharedObj){  
            while(this.threadSignal){
                sharedObj.i++;                 this.runOnUiThread(function(sharedObj){
                    W("body ul").appendChild("<li>"+sharedObj.i+"</li>");
                });                 this.sleep(500);
            }
            return sharedObj.i;
        }, function(r){
            console.log("t2>" + r.returnValue + ":" + r.error);
        });

This usage gives the code good structure, flexibility, and maintainability both formally and semantically.

Ok, so much for the discussion on the usage of Web Worker. If you are interested, you can take a look at this project: https://github.com/akira-cn/WorkerThread.js (because the Worker needs to use the server to test, I put a fake httpd.js in the project, which is a very simple HTTP service js, you can run with node directly).


Related articles: