Aujourd'hui, nous allons travailler avec les workers javascript. Vous le savez certainement, Javascript est un langage mono-threadé, ce qui peut poser problème si on fait de nombreux calculs : l'interface risque de freezer.

Pour éviter ces freezes, Javascript permet maintenant de créer des workers. Le principe est d'avoir du code javascript dans des fichiers séparés, qui s'exécuteront en dehors du thread principal - en pouvant passer des messages d'un thread à l'autre.
Si vous ne connaissez pas, je vous invite à lire cette introduction aux Web Workers par David Rousset.

Tout ça fonctionne très bien, mais la communication entre les threads étant limitée à des objets string ou json n'est pas pratique : on doit poster un message pour lancer une fonction, et attendre un message disant que c'est fini. Je vais vous montrer comment se baser sur les workers pour pouvoir faire des appels direct de fonctions et passer des callbacks.

Le code source complet de cet article est disponible sur mon compte Github

1ère étape : appeler les fonctions en arrière plan

Pour commencer, je vais mettre mon code à exécuter en arrière plan dans un fichier séparé : je crée un objet Code qui contient plusieurs fonctions dans un fichier code.js :

var Code = {
    MakeLotsOfCalculations: function (count) {
        var result = 0;
        for (var i = 0; i < count; i++) {
            var a = Math.exp(i) + Math.cos(i);
            result += i;
        }
        return result;
    },

    DoSomethingFast: function () {
        return "hello";
    }
};

Et je crée mon fichier worker.js, qui va attendre des messages pour appeler les fonctions de code.js :

importScripts("code.js");

onmessage = function messageHandler(event) {

    if (event.data) {
        var data = event.data;

        var fn = data.functionName;
        var result = Code[fn].apply(null, data.args);

        postMessage(result);
    }
}

Le fonctionnement est assez simple, on a un fichier qui contient nos fonctions, et pour activer l'une de ces fonctions dans le worker, on lui envoie un message avec le nom de la fonction, et éventuellement les paramètres. Lorsque le traitement est fini, le worker renvoie un message avec le résultat :

var worker = new Worker('js/worker.js');

worker.onmessage = function(event){
	// event.data contains the result
} 

var message = {
	functionName: "DoSomethingFast",
};
worker.postMessage(message);

Ce que je veux, c'est avoir des fonctions que je peux appeler directement, je crée donc un objet qui va contenir ces fonctions. Je pourrais créer ces fonctions à la main, mais je peux directement boucler sur mon objet Code pour avoir le nom des différentes fonctions.

for (var fn in Code) {
    if (Code[fn] && typeof Code[fn] === "function") {
        BackgroundWorker[fn] = callFunctionAsync(fn);
    }
}

Ici, fn correspond au nom de chacune des fonctions, et callFunctionAsync est une fonction qui retourne une fonction (faut être concentré !) qui va envoyer le message au worker. Les arguments ajoutés au message sont directement ceux de la fonction, grâce au mot clé arguments. Enfin, presque1

function callFunctionAsync(functionName) {
    return function () {           
        var message = {
            functionName: functionName,
            args: arguments
        };

        worker.postMessage(message);
    }
} 

Ainsi, je peux appeler chacune des fonctions de l'objet Code directement dans l'objet BackgroundWorker, et le code sera exécuté en arrière plan.

BackgroundWorker.MakeLotsOfCalculations(150);

2ème étape : récupérer le résultat

Maintenant qu'on a réussi à exécuter du code dans notre worker, on va vouloir être notifié lorsque le traitement est fini, et récupérer le résultat. Pour cela, on va utiliser des callbacks.

Le principe ici : lorsque notre fonction originale avait n arguments, la fonction qu'on appelle dans notre background worker en aura n+1, le dernier argument sera une fonction à appeler à la fin du traitement, avec le résultat qui sera passé en paramètre :

BackgroundWorker.MakeLotsOfCalculations(150, function callback(result){
	alert(result);
});

Pour faire ça, il va falloir faire une première passe sur les arguments, afin d'isoler le callback. On crée une fonction qui va prendre les paramètres, et renvoyer un objet avec les vrais arguments (sous forme de tableau2) et le callback.

function checkFunctionArgs(originalArgs) {
    var args = [];
    var callback = null;
    for (var fnArg in originalArgs) {
        if (typeof originalArgs[fnArg] === "function") {
            callback = originalArgs[fnArg];
        } else {
            args.push(originalArgs[fnArg]);
        }
    }
    return {
        args: args,
        callback: callback
    };
}

Au moment de l'appel de la fonction, je vais mettre le callback de coté pour pouvoir le rappeler lorsqu'on reçoit la réponse du worker.

Le worker, même s'il est dans un thread séparé, est lui-même mono-threadé - ce qui signifie que si on appelle plusieurs fonctions, elles seront traitées les unes après les autres. Ceci va nous simplifier la vie, puisqu'on n'a qu'à garder une liste de callbacks dans l'ordre pour pouvoir gérer leurs retours. On va donc créer une file (FIFO - le premier callback enregistré sera le premier à être appelé) pour les stocker, et modifier la fonction callFunctionAsync :

var callbackQueue = [];

function callFunctionAsync(functionName) {
    return function () {
        var functionArgs = checkFunctionArgs(arguments);
        callbackQueue.push(functionArgs.callback);

        var message = {
            functionName: functionName,
            args: functionArgs.args
        };

        worker.postMessage(message);
    }
}

Et lorsque le worker poste un message, on va pouvoir renvoyer le résultat :

worker.onmessage = function (event) {
    var callback = callbackQueue.shift();
    if (callback && typeof callback === "function") {
        var result = event.data;
        callback(result);
    }
}

Et voilà, maintenant les workers sont beaucoup plus faciles à utiliser !

Encore une fois, n'hésitez pas à récupérer le code source complet sur Github - il contient tout ce qu'on a vu, avec des exemples d'utilisation, ainsi qu'un fallback pour les navigateurs ne gérant pas les WebWorkers.


[1] En fait, ça serait bien que ça soit aussi simple, mais ça ne fonctionnera pas. Le mot clé arguments a l'air d'un tableau, mais n'en est pas un, et il ne pourra pas être sérialisé correctement. Il faut créer un nouveau tableau contenant toutes les valeurs.
Ce point sera corrigé un peu plus bas dans l'article.

[2] Et comme promis, on en profite pour corriger le bug de la note 1