Concepts avancés et exemples

Cette traduction est incomplète. Aidez à traduire cet article depuis l'anglais.

Cet article fournit de nombreux détails et maints exemples pour illustrer les concepts avancés des web workers.

Passage de données : copie, et non partage

Les données passées entre la page principale et les workers sont copiées, et non partagées. Les objets sont sérialisées au moment où ils sont confiés au worker, et consécutivement désérialisés à l'autre bout. La page et le worker ne partagent pas la même instance, ainsi au final une copie est créée de chaque côté. La plupart des navigateurs implémentent cette caractéristique en tant que clonage structuré.

Avant de poursuivre, créons à des fins didactiques une fonction nommée emulateMessage(), avec pour objectif de simuler le comportement d'une valeur qui est clonée et non partagée durant le passage du worker à la page principale ou inversement :

function emulateMessage (vVal) {
    return eval("(" + JSON.stringify(vVal) + ")");
}

// Tests

// test #1
var example1 = new Number(3);
alert(typeof example1); // objet
alert(typeof emulateMessage(example1)); // nombre

// test #2
var example2 = true;
alert(typeof example2); // booléen
alert(typeof emulateMessage(example2)); // booléen

// test #3
var example3 = new String("Hello World");
alert(typeof example3); // objet
alert(typeof emulateMessage(example3)); // chaîne de caractères

// test #4
var example4 = {
    "name": "John Smith",
    "age": 43
};
alert(typeof example4); // objet
alert(typeof emulateMessage(example4)); // objet

// test #5
function Animal (sType, nAge) {
    this.type = sType;
    this.age = nAge;
}
var example5 = new Animal("Cat", 3);
alert(example5.constructor); // Animal
alert(emulateMessage(example5).constructor); // Objet

Une valeur qui est clonée et non partagée est appelée message. Comme vous le savez probablement dès à présent, les messages peuvent être envoyés à et à partir du thread principal en utilisant postMessage(), et l'attribut data de l'événement message contient les données retournées par le worker.

example.html (la page principale) :

var myWorker = new Worker("my_task.js");

myWorker.onmessage = function (oEvent) {
  console.log("Worker said : " + oEvent.data);
};

myWorker.postMessage("ali");

my_task.js (leworker) :

postMessage("I\'m working before postMessage(\'ali\').");

onmessage = function (oEvent) {
  postMessage("Hi " + oEvent.data);
};

L'algorithme de clonage structurée peut accepter du JSON et quelques autres choses impossibles en JSON — comme les références circulaires.

Exemples de passages de données

Exemple #1 : Créer un "eval() asynchrone" générique

L'exemple suivant montre comment utiliser un worker afin d'exécuter de manière asynchrone n'importe quel code JavaScript permis dans un worker, au moyen d'une méthode eval() appelée dans le worker :

// Syntaxe : asyncEval(code[, listener])

var asyncEval = (function () {

  var aListeners = [], oParser = new Worker("data:text/javascript;charset=US-ASCII,onmessage%20%3D%20function%20%28oEvent%29%20%7B%0A%09postMessage%28%7B%0A%09%09%22id%22%3A%20oEvent.data.id%2C%0A%09%09%22evaluated%22%3A%20eval%28oEvent.data.code%29%0A%09%7D%29%3B%0A%7D");

  oParser.onmessage = function (oEvent) {
    if (aListeners[oEvent.data.id]) { aListeners[oEvent.data.id](oEvent.data.evaluated); }
    delete aListeners[oEvent.data.id];
  };


  return function (sCode, fListener) {
    aListeners.push(fListener || null);
    oParser.postMessage({
      "id": aListeners.length - 1,
      "code": sCode
    });
  };

})();

La data URI est équivalente à une requête réseau, avec la réponse suivante :

onmessage = function (oEvent) {
	postMessage({
		"id": oEvent.data.id,
		"evaluated": eval(oEvent.data.code)
	});
}

Exemples d'utilisation :

// message d'alerte asynchrone...
asyncEval("3 + 2", function (sMessage) {
    alert("3 + 2 = " + sMessage);
});

// affichage asynchrone d'un message...
asyncEval("\"Hello World!!!\"", function (sHTML) {
    document.body.appendChild(document.createTextNode(sHTML));
});

// néant asynchrone...
asyncEval("(function () {\n\tvar oReq = new XMLHttpRequest();\n\toReq.open(\"get\", \"http://www.mozilla.org/\", false);\n\toReq.send(null);\n\treturn oReq.responseText;\n})()");

Exemple #2 : passage avancé de données JSON et création d'un système d'échange

Si vous devez passer des données complexes et appeler différentes fonctions à la fois dans la page principale et dans le worker, vous pouvez créer un système comme suit.

example.html (la page principale) :

<!doctype html>
<html>
<head>
<meta charset="UTF-8"  />
<title>MDN Example - Queryable worker</title>
<script type="text/javascript">
  /*
    QueryableWorker instances methods:
     * sendQuery(queryable function name, argument to pass 1, argument to pass 2, etc. etc): calls a Worker's queryable function
     * postMessage(string or JSON Data): see Worker.prototype.postMessage()
     * terminate(): terminates the Worker
     * addListener(name, function): adds a listener
     * removeListener(name): removes a listener
    QueryableWorker instances properties:
     * defaultListener: the default listener executed only when the Worker calls the postMessage() function directly
  */
  function QueryableWorker (sURL, fDefListener, fOnError) {
    var oInstance = this, oWorker = new Worker(sURL), oListeners = {};
    this.defaultListener = fDefListener || function () {};
    oWorker.onmessage = function (oEvent) {
      if (oEvent.data instanceof Object && oEvent.data.hasOwnProperty("vo42t30") && oEvent.data.hasOwnProperty("rnb93qh")) {
        oListeners[oEvent.data.vo42t30].apply(oInstance, oEvent.data.rnb93qh);
      } else {
        this.defaultListener.call(oInstance, oEvent.data);
      }
    };
    if (fOnError) { oWorker.onerror = fOnError; }
    this.sendQuery = function (/* nom de la fonction requêtable, argument à passer 1, argument à passer 2, etc. etc */) {
      if (arguments.length < 1) { throw new TypeError("QueryableWorker.sendQuery - not enough arguments"); return; }
      oWorker.postMessage({ "bk4e1h0": arguments[0], "ktp3fm1": Array.prototype.slice.call(arguments, 1) });
    };
    this.postMessage = function (vMsg) {
      //Je ne pense pas qu'il y ait besoin d'appeler la méthode call()
      //que diriez-vous tout simplement de oWorker.postMessage(vMsg);
      //le même cas se pose avec terminate
      //bien, juste un peu plus vite, aucune recherche dans la chaîne des prototypes
      Worker.prototype.postMessage.call(oWorker, vMsg);
    };
    this.terminate = function () {
      Worker.prototype.terminate.call(oWorker);
    };
    this.addListener = function (sName, fListener) {
      oListeners[sName] = fListener;
    };
    this.removeListener = function (sName) {
      delete oListeners[sName];
    };
  };

  // votre worker "queryable" personnalisé
  var oMyTask = new QueryableWorker("my_task.js" /* , votreEcouteurDeMessageParDefautIci [optional], votreEcouteurDErreurIci [optional] */);

  // vos "écouteurs" personnalisés

  oMyTask.addListener("printSomething", function (nResult) {
    document.getElementById("firstLink").parentNode.appendChild(document.createTextNode(" The difference is " + nResult + "!"));
  });

  oMyTask.addListener("alertSomething", function (nDeltaT, sUnit) {
    alert("Worker waited for " + nDeltaT + " " + sUnit + " :-)");
  });
</script>
</head>
<body>
  <ul>
    <li><a id="firstLink" href="javascript:oMyTask.sendQuery('getDifference', 5, 3);">What is the difference between 5 and 3?</a></li>
    <li><a href="javascript:oMyTask.sendQuery('waitSomething');">Wait 3 seconds</a></li>
    <li><a href="javascript:oMyTask.terminate();">terminate() the Worker</a></li>
  </ul>
</body>
</html>

my_task.js (le worker) :

// vos fonctions PRIVEES personnalisées

function myPrivateFunc1 () {
  // instructions à exécuter
}

function myPrivateFunc2 () {
  // instructions à exécuter
}

// etc. etc.

// vos fonctions PUBLIQUES personnalisées (i.e. requêtables depuis la page principale)

var queryableFunctions = {
  // exemple #1 : obtenir la différence entre deux nombres :
  getDifference: function (nMinuend, nSubtrahend) {
      reply("printSomething", nMinuend - nSubtrahend);
  },
  // exemple #2 : attendre trois secondes
  waitSomething: function () {
      setTimeout(function() { reply("alertSomething", 3, "seconds"); }, 3000);
  }
};

// fonctions système

function defaultQuery (vMsg) {
  // votre fonction PUBLIQUE par défaut est exécutée seulement lorsque la page principale appelle la méthode queryableWorker.postMessage() directement
  // instructions à exécuter
}

function reply (/* listener name, argument to pass 1, argument to pass 2, etc. etc */) {
  if (arguments.length < 1) { throw new TypeError("reply - not enough arguments"); return; }
  postMessage({ "vo42t30": arguments[0], "rnb93qh": Array.prototype.slice.call(arguments, 1) });
}

onmessage = function (oEvent) {
  if (oEvent.data instanceof Object && oEvent.data.hasOwnProperty("bk4e1h0") && oEvent.data.hasOwnProperty("ktp3fm1")) {
    queryableFunctions[oEvent.data.bk4e1h0].apply(self, oEvent.data.ktp3fm1);
  } else {
    defaultQuery(oEvent.data);
  }
};

Il est possible d'échanger le contenu de chaque message page principale -> worker et worker -> page principale.

Passage de données par transfert de propriété (objets transférables)

Google Chrome 17+ et Firefox 18+ proposent une manière additionnelle de passer certains types d'objets (les objets transférables, c'est-à-dire les objets implémentant l'interface Transferable) vers ou à partir d'un worker avec une haute performance. Les objets transférables sont transférés d'un contexte vers un autre sans aucune opération de copie, ce qui conduit à d'énormes gains de performance lorsque de gros ensembles de données sont envoyés. Considérez la chose comme un passage par référence si vous venez du monde C/C++. Cependant, contrairement au passage par référence, la 'version' issue du contexte appelant n'est plus disponible une fois transférée. Sa propriété est transférée au nouveau contexte. Par exemple, lors du transfert d'un ArrayBuffer à partir de votre application principale vers le script d'un worker, le ArrayBuffer original est nettoyé et définitivement inutilisable. Son contenu est (tout à fait littéralement) transféré au contexte du worker.

// Crée un "fichier" de 32MB et le remplit.
var uInt8Array = new Uint8Array(1024*1024*32); // 32MB
for (var i = 0; i < uInt8Array.length; ++i) {
  uInt8Array[i] = i;
}

worker.postMessage(uInt8Array.buffer, [uInt8Array.buffer]);

Remarque : pour plus d'information sur les objets transférables, la performance et la détection de fonctionnalité de cette méthode, lisez Transferable Objects: Lightning Fast! sur HTML5 Rocks.

Workers embarqués

Il n'y a pas une manière "officielle" d'embarquer le code d'un worker dans une page web, comme les éléments <script> le font pour les scripts normaux. Mais un élément <script> qui n'aurait pas d'attribut src et dont l'attribut type n'identifierait pas un type MIME exécutable peut être considéré comme un élément de bloc de données dont JavaScript peut faire usage.  Les "blocs de données" sont une caractéristique plus générale d'HTML5 qui peuvent contenir presque n'importe quelles données textuelles. Ainsi, un worker pourrait être embarqué de cette façon :

<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8" />
<title>MDN Example - Embedded worker</title>
<script type="text/js-worker">
  // Ce script NE SERA PAS traité par les moteurs JS parce que son type MIME est text/js-worker.
  var myVar = "Hello World!";
  // Le reste du code de votre worker commence ici.
</script>
<script type="text/javascript">
  // Ce script SERA analysé par les moteurs JS engines parce que son type MIME est text/javascript.
  function pageLog (sMsg) {
    // Utilisation d'un fragment : le navigateur réaffichera/réorganisera le DOM seulement une fois.
    var oFragm = document.createDocumentFragment();
    oFragm.appendChild(document.createTextNode(sMsg));
    oFragm.appendChild(document.createElement("br"));
    document.querySelector("#logDisplay").appendChild(oFragm);
  }
</script>
<script type="text/js-worker">
  // Ce script NE SERA PAS traité par les moteurs JS parce que son type MIME est text/js-worker.
  onmessage = function (oEvent) {
    postMessage(myVar);
  };
  // Le reste du code de votre worker commence ici.
</script>
<script type="text/javascript">
  // Ce script SERA analysé par les moteurs JS engines parce que son type MIME est text/javascript.

  // Dans le passé... :
  // blob builder a existé
  // ...mais nous utilisons désormais Blob...:
  var blob = new Blob(Array.prototype.map.call(document.querySelectorAll("script[type=\"text\/js-worker\"]"), function (oScript) { return oScript.textContent; }),{type: "text/javascript"});

  // Création d'une nouvelle propriété document.worker contenant tous nos scripts "text/js-worker".
  document.worker = new Worker(window.URL.createObjectURL(blob));

  document.worker.onmessage = function (oEvent) {
    pageLog("Received: " + oEvent.data);
  };

  // Démarrage du worker.
  window.onload = function() { document.worker.postMessage(""); };
</script>
</head>
<body><div id="logDisplay"></div></body>
</html>

Le worker embarqué est maintenant imbriqué dans une nouvelle propriété personnalisée document.worker.

Exemples

Cette section fournit plusieurs exemples sur la façon d'utiliser les workers DOM.

Réaliser des calculs en arrière-plan

Les workers sont principalement utiles pour permettre à votre code de réaliser des calculs très consommateur en CPU sans bloquer le thread de l'interface utilisateur. Dans cet exemple, un worker est utilisé pour calculer la suite de Fibonacci.

Le code JavaScript

Le code JavaScript suivant est stocké dans le fichier "fibonacci.js" référencé par le fichier HTML dans la prochaine section.

var results = [];

function resultReceiver(event) {
  results.push(parseInt(event.data));
  if (results.length == 2) {
    postMessage(results[0] + results[1]);
  }
}

function errorReceiver(event) {
  throw event.data;
}

onmessage = function(event) {
  var n = parseInt(event.data);

  if (n == 0 || n == 1) {
    postMessage(n);
    return;
  }

  for (var i = 1; i <= 2; i++) {
    var worker = new Worker("fibonacci.js");
    worker.onmessage = resultReceiver;
    worker.onerror = errorReceiver;
    worker.postMessage(n - i);
  }
 };

Le worker affecte à la propriété onmessage  une fonction qui recevra les messages envoyés lorsque la méthode  postMessage() de l'objet worker est appelée (remarquez que cela diffère de définir une variable globale de ce nom, ou de définir une fonction avec ce nom.   var onmessage et function onmessage définissent des propriétés globales avec ces noms, mais elles n'enregistrent pas la fonction pour recevoir les messages envoyés par la page web qui a créé le worker). Au démarrage de la récursion, il engendre ainsi de nouvelles copies de lui-même pour gérer chacune des itérations du calcul.

Le code HTML

<!DOCTYPE html>
<html>
  <head>
    <meta charset="UTF-8"  />
    <title>Test threads fibonacci</title>
  </head>
  <body>

  <div id="result"></div>

  <script language="javascript">

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

    worker.onmessage = function(event) {
      document.getElementById("result").textContent = event.data;
      dump("Got: " + event.data + "\n");
    };

    worker.onerror = function(error) {
      dump("Worker error: " + error.message + "\n");
      throw error;
    };

    worker.postMessage("5");

  </script>
  </body>
</html>

La page web crée un élément div avec l'ID  result , qui sera utilisé pour afficher le résultat, puis engendre le worker.  Après création du worker, le gestionnaire onmessage est configuré pour afficher les résultats en renseignant le contenu de l'élément div, et le gestionnaire onerror est configuré pour capturer le message d'erreur.

Finalement, un message est envoyé au worker pour le démarrer.

Tester cet exemple.

Réaliser des E/S web en arrière-plan

Vous pouvez trouver un tel exemple dans l'article Using workers in extensions .

Répartir des tâches entre plusieurs workers

Les ordinateurs multi-coeur étant de plus en plus répandus, il est souvent utile de répartir le calcul de tâches complexes entre différents workers afin de tirer partie des coeurs de ces multiprocesseurs.

Voir aussi

Étiquettes et contributeurs liés au document

 Contributeurs à cette page : jean-pierre.gay
 Dernière mise à jour par : jean-pierre.gay,