Использование Web Workers

Перевод не завершен. Пожалуйста, помогите перевести эту статью с английского.

Web Workers предоставляют простое средство для запуска скриптов в фоновом потоке. Поток Worker'а может выполнять задачи без вмешательства в пользовательский интерфейс. К тому же, они могут осуществлять ввод/вывод, используя XMLHttpRequest (хотя атрибуты responseXML и channel всегда будут равны null). Существующий Worker может отсылать сообщения в JavaScript код, который его создал, отправляя сообщения в обработчик событий, указанный этим кодом (и наоборот). Эта статья дает детальную инструкцию по использованию Web Workers.

Web Workers API

Worker - это объект, созданный при помощи конструктора (например, Worker()), который запускает JavaScript файл по имени — этот файл содержит код, который будет выполнен в потоке Worker'а; объекты Workers запускаются в другом глобальном контексте, который отличается от текущего window. Таким образом, использование переменной window для получения текущего глобального контекста (вместо self) внутри Worker вернет ошибку.

Контекст Worker'а представлен объектом DedicatedWorkerGlobalScope в случае выделенного Workers (обычные Workers, которые используются одним скриптом; Совместные Workers используют объект SharedWorkerGlobalScope). Выделенный Worker доступен только из скрипта, который первый породил его, в то время, как совместные Workers могут быть доступны из нескольких сценариев.

Примечание: Смотри страницу Web Workers API для справки по Workers и прочие руководства.

Вы можете запускать какой угодно код который вам нравится внутри потока worker-а, с некоторыми исключениями. Например, вы не можете прямо манипулировать DOM внутри worker-а, или использовать некоторые методы по умолчанию и свойства объекта window. Но вы можете использовать большой набор пунктов доступных под window, включая WebSockets, и механизмы хранения данных таких как IndexedDB и относящихся только к Firefox OS Data Store API. Смотрите Functions and classes available to workers для больших деталей.

Данные передаются между worker-ами и главным потоком через систему сообщений — обе стороны передают свои сообщения используя метод postMessage(), и отвечают на сообщения при помощи обработчика событий onmessage (сообщение хранится в аттрибуте data события Message.) Данные скорее копируються, чем деляться.

Объекты Workers могут, в свою очередь, создавать новые объекты workers, и так до тех пор, пока всё работает в рамках текущей страницы. Плюс к этому, объекты workers могут использовать XMLHttpRequest для сетевого ввода/вывода, но есть исключение - атрибуты responseXML и channel объекта XMLHttpRequest всегда возвращают null.

Выделенные Workers

Как уже упоминалось выше, выделенный Worker доступен только для скрипта, который его вызвал. В этом разделе речь пойдет о JavaScript, который можно найти в нашем основном примере выделенного Worker (запустить скрипт): Этот пример позволяет ввести два числа для умножения. Эти числа отправляются в Worker, перемножаются, а результат возвращается на страницу и отображается.

Этот пример скорее банальный, но мы решили оставить его простым пока вводя вас к базовым концепциям worker-ов. Более продвинутые детали описаны позже в статье.

Определение поддержки Worker

Для большего контроля обработки ошибок и обратной совместимости, хорошая идея обернуть ваш код доступа worker-а в следующем (main.js):

if (window.Worker) {

  ...

}

Создание выделенного worker

Создание нового worker-а — это легко. Всё что вам нужно это вызвать конструктор Worker(), уточнив URI скрипта для выполнения в потоке worker-а (main.js):

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

Передача сообщений в и из выделенного worker

Магия worker-ов происходит через postMessage() метод и обработчик событий onmessage. Когда вы хотите отправить сообщение в worker, вы доставляете сообщение к нему вот так (main.js):

first.onchange = function() {
  myWorker.postMessage([first.value,second.value]);
  console.log('Message posted to worker');
}

second.onchange = function() {
  myWorker.postMessage([first.value,second.value]);
  console.log('Message posted to worker');
}

В приведенном фрагменте кода мы имеем два <input> элемента, определяющих значение переменных first и second; когда значение  любой из переменных изменяется,   myWorker.postMessage([first.value,second.value]) используется для отправки обоих значений, представленных в виде массива, в worker. Посредством аргумента message возможна передача практически любых данных в worker.

Внутри worker-a мы можем обрабатывать сообщения и отвечать на них при помощи добавления обработчика события onmessage подобным образом (worker.js):

onmessage = function(e) {
  console.log('Message received from main script');
  var workerResult = 'Result: ' + (e.data[0] * e.data[1]);
  console.log('Posting message back to main script');
  postMessage(workerResult);
}

Обработчик onmessage позволяет нам запустить некий код всякий раз, когда получено сообщение, вместе с этим сообщением доступным в аттрибуте события сообщения data. В примере выше мы просто перемножаем вместе две цифры, после чего используем postMessage() снова, чтобы отправить полученый результат назад в основной поток.

Возвращаясь в основной поток, мы используем onmessage снова, чтобы отреагировать на сообщение отправленное нам назад из worker:

myWorker.onmessage = function(e) {
  result.textContent = e.data;
  console.log('Message received from worker');
}

В примере выше мы хватаем данные из события сообщения и устанавливаем эти данные как textContent у результирующего параграфа, таким образом пользователь может видеть результат этой калькуляции.

Заметка: The URI passed as a parameter to the Worker constructor must obey the same-origin policy .

There is currently disagreement among browsers vendors on what URIs are of the same-origin; Gecko 10.0 (Firefox 10.0 / Thunderbird 10.0 / SeaMonkey 2.7) and later do allow data URIs and Internet Explorer 10 does not allow Blob URIs as a valid script for workers.

Заметка: Notice that onmessage and postMessage() need to be hung off the Worker object when used in the main script thread, but not when used in the worker. This is because, inside the worker, the worker is effectively the global scope.
Заметка: When a message is passed between the main thread and worker, it is copied or "transferred" (moved), not shared. Read Transferring data to and from workers: further details for a much more thorough explanation.

Завершить worker

Если необходимо немедленно завершить запущенный worker из главного потока, вы можете сделать так вызвав метод worker-а terminate:

myWorker.terminate();

Поток worker-а умирает немедленно без возможности завершить свои операции или убрать за собой.

В потоке worker-а, worker-ы могут зыкрыть себя вызвав их метод close:

close();

Обработка ошибок

Когда происходит ошибка времени исполнения в worker-е, вызывается его обработчик событий onerror. Он принимает событие называющееся error которое реализует интерфейс ErrorEvent.

The event doesn't bubble and is cancelable; to prevent the default action from taking place, the worker can call the error event's preventDefault() method.

The error event has the following three fields that are of interest:

message
A human-readable error message.
filename
The name of the script fil in which the error occurred.
lineno
The line number of the script file on which the error occurred.

Создание subworkers

Workers may spawn more workers if they wish. So-called sub-workers must be hosted within the same origin as the parent page. Also, the URIs for subworkers are resolved relative to the parent worker's location rather than that of the owning page. This makes it easier for workers to keep track of where their dependencies are.

Импорт скриптов и библиотек

Worker threads have access to a global function, importScripts(), which lets them import scripts in the same domain into their scope. It accepts zero or more URIs as parameters to resources to import; all of the following examples are valid:

importScripts();                        /* imports nothing */
importScripts('foo.js');                /* imports just "foo.js" */
importScripts('foo.js', 'bar.js');      /* imports two scripts */

The browser loads each listed script and executes it. Any global objects from each script may then be used by the worker. If the script can't be loaded, NETWORK_ERROR is thrown, and subsequent code will not be executed. Previously executed code (including code deferred using window.setTimeout()) will still be functional though. Function declarations after the importScripts() method are also kept, since these are always evaluated before the rest of the code.

Note: Scripts may be downloaded in any order, but will be executed in the order in which you pass the filenames into importScripts() . This is done synchronously; importScripts() does not return until all the scripts have been loaded and executed.

Разделяемые worker-ы

A shared worker is accessible by multiple scripts — even if they are being accessed by different windows, iframes or even workers. In this section we'll discuss the JavaScript found in our Basic shared worker example (run shared worker): This is very similar to the basic dedicated worker example, except that it has two functions available handled by different script files: multiplying two numbers, or squaring a number. Both scripts use the same worker to do the actual calculation required.

Here we'll concentrate on the differences between dedicated and shared workers. Note that in this example we have two HTML pages, each with JavaScript applied that uses the same single worker file.

Note: If SharedWorker can be accessed from several browsing contexts, all those browsing contexts must share the exact same origin (same protocol, host, and port).

Note: In Firefox, shared workers cannot be shared between documents loaded in private and non-private windows (баг 1177621).

Создание разделяемого worker-а

Spawning a new worker is pretty much the same as with a dedicated worker, but with a different constructor name (see index.html and index2.html) — each one has to spin up the worker using code like the following:

var myWorker = new SharedWorker("worker.js");

One big difference is that with a shared worker you have to communicate via a port object — an explicit port is opened that the scripts can use to communicate with the worker (this is done implicitly in the case of dedicated workers).

The port connection needs to be started either implicitly by use of the onmessage event handler or explicitly with the start() method before any messages can be posted. Although the multiply.js and worker.js files in the demo currently call the start() method, those calls are not necessary since the onmessage event handler is being used. Calling start() is only needed if the message event is wired up via the addEventListener() method.

When using the start() method to open the port connection, it needs to be called from both the parent thread and the worker thread if two-way communication is needed.

myWorker.port.start();  // called in parent thread
port.start();  // called in worker thread, assuming the port variable references a port

Передача сообщений в и из разделяемого worker-а

Now messages can be sent to the worker as before, but the postMessage() method has to be invoked through the port object (again, you'll see similar constructs in both multiply.js and square.js):

squareNumber.onchange = function() {
  myWorker.port.postMessage([squareNumber.value,squareNumber.value]);
  console.log('Message posted to worker');
}

Now, on to the worker. There is a bit more complexity here as well (worker.js):

self.addEventListener('connect', function(e) { // addEventListener() is needed
  var port = e.ports[0];
  port.onmessage = function(e) {
    var workerResult = 'Result: ' + (e.data[0] * e.data[1]);
    port.postMessage(workerResult);
  }
  port.start();  // not necessary since onmessage event handler is being used
});

First, we use an onconnect handler to fire code when a connection to the port happens (i.e. when the onmessage event handler in the parent thread is setup, or when the start() method is explicitly called in the parent thread).

We use the ports attribute of this event object to grab the port and store it in a variable.

Next, we add a message handler on the port to do the calculation and return the result to the main thread. Setting up this message handler in the worker thread also implicitly opens the port connection back to the parent thread, so the call to port.start() is not actually needed, as noted above.

Finally, back in the main script, we deal with the message (again, you'll see similar constructs in both multiply.js and square.js):

myWorker.port.onmessage = function(e) {
  result2.textContent = e.data[0];
  console.log('Message received from worker');
}

When a message comes back through the port from the worker, we check what result type it is, then insert the calculation result inside the appropriate result paragraph.

О потоковой безопасности

The Worker interface spawns real OS-level threads, and mindful programmers may be concerned that concurrency can cause “interesting” effects in your code if you aren't careful.

However, since web workers have carefully controlled communication points with other threads, it's actually very hard to cause concurrency problems. There's no access to non-threadsafe components or the DOM. And you have to pass specific data in and out of a thread through serialized objects. So you have to work really hard to cause problems in your code.

Передача данных в и из worker-ов: другие детали

Data passed between the main page and workers is copied, not shared. Objects are serialized as they're handed to the worker, and subsequently, de-serialized on the other end. The page and worker do not share the same instance, so the end result is that a duplicate is created on each end. Most browsers implement this feature as structured cloning.

To illustrate this, let's create for didactical purpose a function named emulateMessage(), which will simulate the behavior of a value that is cloned and not shared during the passage from a worker to the main page or vice versa:

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

// Tests

// test #1
var example1 = new Number(3);
console.log(typeof example1); // object
console.log(typeof emulateMessage(example1)); // number

// test #2
var example2 = true;
console.log(typeof example2); // boolean
console.log(typeof emulateMessage(example2)); // boolean

// test #3
var example3 = new String("Hello World");
console.log(typeof example3); // object
console.log(typeof emulateMessage(example3)); // string

// test #4
var example4 = {
    "name": "John Smith",
    "age": 43
};
console.log(typeof example4); // object
console.log(typeof emulateMessage(example4)); // object

// 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); // Object

A value that is cloned and not shared is called message. As you will probably know by now, messages can be sent to and from the main thread by using postMessage(), and the message event's data attribute contains data passed back from the worker.

example.html: (the main page):

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

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

myWorker.postMessage("ali");

my_task.js (the worker):

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

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

The structured cloning algorithm can accept JSON and a few things that JSON can't — like circular references.

Примеры передачи данных

Example #1: Create a generic "asynchronous eval()"

The following example shows how to use a worker in order to asynchronously execute any JavaScript code allowed in a worker, through eval() within the worker:

// Syntax: 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
    });
  };
})();

The data URL is equivalent to a network request, with the following response:

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

Sample usage:

// asynchronous alert message...
asyncEval("3 + 2", function (sMessage) {
    alert("3 + 2 = " + sMessage);
});

// asynchronous print message...
asyncEval("\"Hello World!!!\"", function (sHTML) {
    document.body.appendChild(document.createTextNode(sHTML));
});

// asynchronous void...
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})()");

Example #2: Advanced passing JSON Data and creating a switching system

If you have to pass some complex data and have to call many different functions both on the main page and in the Worker, you can create a system like the following.

example.html (the main page):

 

MDN Example - Queryable worker

my_task.js (the worker):

// your custom PRIVATE functions

function myPrivateFunc1 () {
  // do something
}

function myPrivateFunc2 () {
  // do something
}

// etc. etc.

// your custom PUBLIC functions (i.e. queryable from the main page)

var queryableFunctions = {
  // example #1: get the difference between two numbers:
  getDifference: function (nMinuend, nSubtrahend) {
      reply("printSomething", nMinuend - nSubtrahend);
  },
  // example #2: wait three seconds
  waitSomething: function () {
      setTimeout(function() { reply("alertSomething", 3, "seconds"); }, 3000);
  }
};

// system functions

function defaultQuery (vMsg) {
  // your default PUBLIC function executed only when main page calls the queryableWorker.postMessage() method directly
  // do something
}

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);
  }
};

It is possible to switch the content of each mainpage -> worker and worker -> mainpage message.

Передача данных с помощью передачи владения (передаваемые объекты)

Google Chrome 17+ and Firefox 18+ contain an additional way to pass certain types of objects (transferable objects, that is objects implementing the Transferable interface) to or from a worker with high performance. Transferable objects are transferred from one context to another with a zero-copy operation, which results in a vast performance improvement when sending large data sets. Think of it as pass-by-reference if you're from the C/C++ world. However, unlike pass-by-reference, the 'version' from the calling context is no longer available once transferred. Its ownership is transferred to the new context. For example, when transferring an ArrayBuffer from your main app to a worker script, the original ArrayBuffer is cleared and no longer usable. Its content is (quite literally) transferred to the worker context.

// Create a 32MB "file" and fill it.
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]);

Note: For more information on transferable objects, performance, and feature-detection for this method, read Transferable Objects: Lightning Fast! on HTML5 Rocks.

Встроенные worker-ы

There is not an "official" way to embed the code of a worker within a web page, like <script> elements do for normal scripts. But a <script> element that does not have a src attribute and has a type attribute that does not identify an executable mime-type can be considered a data block element that JavaScript could use. "Data blocks" is a more general feature of HTML5 that can carry almost any textual data. So, a worker could be embedded in this way:

 

MDN Example - Embedded worker

 

The embedded worker is now nested into a new custom document.worker property.

Другие примеры

This section provides further examples of how to use web workers.

Выполнение вычислений в фоне

Workers are mainly useful for allowing your code to perform processor-intensive calculations without blocking the user interface thread. In this example, a worker is used to calculate Fibonacci numbers.

The JavaScript code

The following JavaScript code is stored in the "fibonacci.js" file referenced by the HTML in the next 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);
  }
 };

The worker sets the property onmessage to a function which will receive messages sent when the worker object's postMessage() is called (note that this differs from defining a global variable of that name, or defining a function with that name. var onmessage and function onmessage will define global properties with those names, but they will not register the function to receive messages sent by the web page that created the worker). This starts the recursion, spawning new copies of itself to handle each iteration of the calculation.

HTML код

 

Test threads fibonacci

 

The web page creates a div element with the ID result , which gets used to display the result, then spawns the worker. After spawning the worker, the onmessage handler is configured to display the results by setting the contents of the div element, and the onerror handler is set to dump the error message.

Finally, a message is sent to the worker to start it.

Try this example.

Выполнение веб I/O в фоне

You can find an example of this in the article Using workers in extensions .

Разделение задач между множественными worker-ами

As multi-core computers become increasingly common, it's often useful to divide computationally complex tasks among multiple workers, which may then perform those tasks on multiple-processor cores.

Другие типы worker-ов

In addition to dedicated and shared web workers, there are other types of worker available:

  • ServiceWorkers essentially act as proxy servers that sit between web applications, and the browser and network (when available). They are intended to (amongst other things) enable the creation of effective offline experiences, intercepting network requests and taking appropriate action based on whether the network is available and updated assets reside on the server. They will also allow access to push notifications and background sync APIs.
  • Chrome Workers are a Firefox-only type of worker that you can use if you are developing add-ons and want to use workers in extensions and have access to js-ctypes in your worker. See ChromeWorker for more details.
  • Audio Workers provide the ability for direct scripted audio processing to be done in a web worker context.

Функции и интерфейсы доступные в worker-ах

You can use most standard JavaScript features inside a web worker, including:

The main thing you can't do in a Worker is directly affect the parent page. This includes manipulating the DOM and using that page's objects. You have to do it indirectly, by sending a message back to the main script via DedicatedWorkerGlobalScope.postMessage, then actioning the changes from there.

Note: For a complete list of functions available to workers, see Functions and interfaces available to workers.

Спецификации

Specification Status Comment
WHATWG HTML Living Standard Живой стандарт No change from Web Workers.
Web Workers Редакторский черновик Initial definition.

Браузерная совместимость

Feature Chrome Firefox (Gecko) Internet Explorer Opera Safari (WebKit)
Basic support 4[1] 3.5 (1.9.1) 10.0 10.6[1] 4[2]
Shared workers 4[1] 29 (29) Нет 10.6 5
Нет 6.1[4]
Passing data using structured cloning 13 8 (8) 10.0 11.5 6
Passing data using transferable objects 17 webkit
21
18 (18) Нет 15 6
Global URL 10[3]
23
21 (21) 11 15 6[3]
Feature Android Chrome for Android Firefox Mobile (Gecko) Firefox OS (Gecko) IE Phone Opera Mobile Safari Mobile
Basic support 4.4 4[1] 3.5 1.0.1 10.0 11.5[1] 5.1[2]
Shared workers Нет 4[1] 8 1.0.1 Нет Нет Нет
Passing data using structured cloning Нет 4 8 1.0.1 Нет Нет Нет
Passing data using transferable objects Нет Нет 18 1.0.1 Нет Нет Нет

[1] Chrome and Opera give an error "Uncaught SecurityError: Failed to construct 'Worker': Script at 'file:///Path/to/worker.js' cannot be accessed from origin 'null'." when you try to run a worker locally. It needs to be on a proper domain.

[2] As of Safari 7.1.2, you can call console.log from inside a worker, but it won't print anything to the console. Older versions of Safari don't allow you to call console.log from inside a worker.

[3] This feature is implemented prefixed as webkitURL.

[4] Safari removed SharedWorker support.

Смотри также

Метки документа и участники

Метки: 
 Внесли вклад в эту страницу: arvitaly, AlexanderTserkovniy, sergeiDruzhinin, kav137, Forshortmrmeth, MuradAz, ahtohbi4, padenot, finalex
 Обновлялась последний раз: arvitaly,