Worker-ы и shared worker-ы

Во всех популярных языках есть потоки (threads). В браузерном javascript для параллельной обработки используются worker-ы.
Под катом рассказ о том, как ими пользоваться, какие ограничения есть в воркерах и об особенностях взаимодействия с ними в разных браузерах.
Отдельный контекст для выполнения фоновых задач, который не блокирует UI. Обычно worker создаётся в виде отдельного скрипта, ресурсы worker-а живут в процессе создавшей его страницы. Shared worker — то же самое, но может быть использован с нескольких страниц.
В worker-е есть:

  • navigator
  • location
  • applicationCache
  • XHR, websocket
  • importScripts для синхронной загрузки скриптов


Worker создаётся из отдельного скрипта:

var worker = new Worker(scriptUrl);
var sharedWorker = new SharedWorker(scriptUrl);


Shared worker идентифицируется по URL. Чтобы создать второй воркер из одного файла, можно добавить какой-нибудь параметр в URL (worker.js? num=2).

Worker можно создать и без отдельного файла. Например, так создать его из текста функции:

var code = workerFn.toString();
code = code.substring(code.indexOf("{")+1, code.lastIndexOf("}"));
var blob = new Blob([code], {type: 'application/javascript'});
worker = new Worker(URL.createObjectURL(blob));

Создать worker из worker-а можно только в Firefox. В Chrome можно создать shared worker из странички и передать его порт другому worker-у (об этом ниже).


DOM


В worker-е нельзя использовать DOM, вместо window глобальный объект называется self. Нельзя получить доступ к localStorage и рисовать на canvas. Такие же ограничения обычно есть во всех десктопных API: доступ к окнам только из UI-треда.

Доступ к объектам


Из worker-ов нельзя вернуть объект. В javascript нет lock-ов и других возможностей потокобезопасности, поэтому из worker-ов нельзя передавать объекты по ссылке, всё отправленное в worker или из него будет скопировано.

CORS


Пока что worker-ы не поддерживают CORS, создать worker можно только загрузив его со своего домена.

Размер стека


Для worker-ов выделяется меньший размер стека, иногда это имеет значение:

Chrome/osx Firefox/osx Safari/osx Chrome/win Firefox/win IE11/win
web 20 800 48 000 63 000 41 900 51 000 63 000
worker 5 300 43 300 6 100 21 300 37 000 30 100


console


До недавнего времени не было, но обычно сейчас уже есть. В некоторых браузерах консоли в worker-ах нет, поэтому перед обращением лучше проверить её доступность.
После создания worker-а ему можно отправить сообщение:

worker.postMessage({hello: 'world'});
worker.onmessage = function(e) { e.data ... };
sharedWorker.port.postMessage({hello: 'world'});
sharedWorker.port.onmessage = function(e) { e.data... };

Подписаться на сообщение в worker-е так:

// worker
self.onmessage = function(e) { e.data... };

// shared worker
self.onconnect = function(e) {
    var port = e.ports[0];
    port.onmessage = function(e) { e.data... };
};

Аналогично и обратно, из worker-а можно вызвать или self.postMessage, или port.postMessage для shared worker-ов.

Метод postMessage использует алгоритм structured clone для клонирования объектов. Это не то же самое, что сериализация в JSON. Алгоритм умеет:

  • копировать RegExp, Blob, File, ImageData
  • восстанавливать циклические ссылки


Но не умеет:

  • Error, Function, DOM-элементы (упадёт ошибка)
  • свойства и прототипы (они не склонируются)

Transferables


Передавать по ссылке кое-что таки можно. Для этого существует второй параметр в postMessage, transferList:

var ab = new ArrayBuffer(size);
worker.postMessage({ data: ab }, [ab]);


В transferList можно передать список объектов, которые будут перемещены. Поддерживаются только ArrayBuffer и MessagePort. В вызывающем контексте объект будет очищен (neutered): у ArrayBuffer будет нулевая длина, и попытка его повторной отправки приведёт к ошибке:

Uncaught DOMException: Failed to execute 'postMessage' on 'Worker': An ArrayBuffer is neutered and could not be cloned.


В Firefox можно создать worker из worker-а (стандарт определяет subworker-ы).
Сейчас в хроме нельзя создать worker из worker-а, а иногда worker-ам надо взаимодействовать между собой. Самый простой способ — сделать передачу сообщений от одного к другому через код страницы. Но это неудобно, потому что: 1. надо писать дополнительный код, 2. в 2 раза увеличивает количество взаимодействий и копирования данных, 3. требует выполнения кода в UI-контексте.
Worker можно научить общаться с shared worker-ом, передав ему порт shared worker-а, при этом передаваемый порт в UI-контексте мы теряем; если он нужен, надо будет переподключиться к shared worker-у, создав его заново. Передача порта выглядит так:

worker.postMessage({ port: sharedWorker.port }, [sharedWorker.port]);
// в worker-е поймать этот порт и сделать что-то с ним


Правда для синхронизации всё равно движком V8 используется UI-контекст, в чём можно убедиться, завесив страничку на какое-то время: worker-ы продолжают работать, а postMessage между ними не ходят, ожидая особождения UI-контекста.
Производительность разная для нескольких случаев, разных размеров данных и использование transferList (trlist):

  • dedicated worker
  • shared worker в создавшем процессе
  • shared worker в другом процессе


В таблице показано количество циклов пересылки данных от worker и обратно в секунду.

Chrome/osx FF/osx Safari/osx Chrome/win FF/win IE11/win
dedicated:10B 9 300 8 400 21 000 6 800 7 300 3 200
dedicated:10kB 4 000 7 000 5 000 3 000 5 000 1 800
dedicated:1MB 80 500 90 60 400 200
dedicated:10MB 8 40 7 7 52 30
dedicated: trlist:10MB 8 400 1 100 2 500 6 200 1 900 2 200
shared:10B 3 100 8 300 - 2 200 5 500 -
shared:10kB 1 800 6 900 - 1 400 4 500 -
shared:1MB 40 500 - 32 400 -
shared:10MB 4 40 - 4 53 -
shared: trlist:10MB - 260 - - 1 800 -
shared-ipc:10B 3 000 - - 2 700 - -
shared-ipc:10kB 1 600 - - 1 700 - -
shared-ipc:1MB 40 - - 30 - -
shared-ipc:10MB 4 - - 3 - -


Выводы, которые можно сделать из данных:

  • затраты на взаимодействие с dedicated worker в хроме меньше, чем с shared;
  • большие объёмы данных намного быстрее передавать через transferList;
  • но всё-таки передача transferList не эквивалентна отправке ссылки или несколких байт.


Decicated worker можно убить, вызвав worker.terminate (). С shared worker так нельзя, его выполнение будет прекращено:

  • когда он закроется сам, вызвав self.close ()
  • когда закроются все странички, его использующие (при этом у worker-а не будет возможности закончить вычисления)
  • когда пользователь принудительно завершит его (например, в хроме из chrome://inspect)
  • когда упадёт или он, или процесс странички, где он живёт


Попробуем вызвать крэш процесса из shared worker-а. Вместе с worker-ом, конечно, упадёт и создавшая его вкладка. Во вкладке, где он ещё использовался, увидим такое сообщение:
3133dbf6751a418a98addd8c74cfe300.png

К сожалению, сейчас нет штатного способа отследить закрытие worker-а или страницы, его использующей.


SharedWorker живёт процессе в страницы, создавшей его. На неё учитывается и показывается в task manager CPU и память, которые потребляет worker. Если страничку закроют, её процесс с worker-ом отдаст память, используемую страницей (не сразу, через некоторое время после закрытия) и останется жить, пока другие страницы используют этот worker. Интересно, что при этом такой процесс полностью исчезнет из статистики хрома: ни память, ни CPU пользоваель не сможет отследить в его внутреннем task manager-е. Это неприятно, т.к. пользователь скорее всего не догадается, почему браузер стал потреблять так много ресурсов.
В chrome shared worker-ы доступны на страничке chrome://inspect/#workers:
aa3cc3db8ee44ebc86779e209830a82a.png
Именно туда пишется вывод console из worker.
Dedicated worker в хроме и IE отлаживается в страничке, на которой он выполняется:
13ff08cf77c244c09ae50171a518459c.png
В других браузерах с отладкой worker-ов пока что плохо.
Поддержка разных worker-ов на Can I Use. Коротко, применительно к сегодняшнему вебу: worker есть на современных браузерах, sharedworker — на продвинутых десктопных браузерах, serviceworker — пока что рано.
Всё написанное актуально на лето 2015 года, не забывайте, что веб быстро меняется.
Using Web Workers (MDN)
The Basics of Web Workers
Living Standard: Web workers
Transferable Objects

© Habrahabr.ru