Pull to refresh

Worker-ы и shared worker-ы

Reading time 5 min
Views 54K
Во всех популярных языках есть потоки (threads). В браузерном javascript для параллельной обработки используются worker-ы.
Под катом рассказ о том, как ими пользоваться, какие ограничения есть в воркерах и об особенностях взаимодействия с ними в разных браузерах.


Что такое worker


Отдельный контекст для выполнения фоновых задач, который не блокирует UI. Обычно worker создаётся в виде отдельного скрипта, ресурсы worker-а живут в процессе создавшей его страницы. Shared worker — то же самое, но может быть использован с нескольких страниц.
В worker-е есть:
  • navigator
  • location
  • applicationCache
  • XHR, websocket
  • importScripts для синхронной загрузки скриптов


Создание worker-а


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-у (об этом ниже).

Ограничения 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-а ему можно отправить сообщение:
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.


Взаимодействие двух worker-ов


В 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-контекста.

Производительность postMessage


Производительность разная для нескольких случаев, разных размеров данных и использование 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 не эквивалентна отправке ссылки или несколких байт.


Убийство worker-а


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

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


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

Учёт ресурсов в shared worker-ах в Chrome


SharedWorker живёт процессе в страницы, создавшей его. На неё учитывается и показывается в task manager CPU и память, которые потребляет worker. Если страничку закроют, её процесс с worker-ом отдаст память, используемую страницей (не сразу, через некоторое время после закрытия) и останется жить, пока другие страницы используют этот worker. Интересно, что при этом такой процесс полностью исчезнет из статистики хрома: ни память, ни CPU пользоваель не сможет отследить в его внутреннем task manager-е. Это неприятно, т.к. пользователь скорее всего не догадается, почему браузер стал потреблять так много ресурсов.

Отладка worker-ов


В chrome shared worker-ы доступны на страничке chrome://inspect/#workers:

Именно туда пишется вывод console из worker.
Dedicated worker в хроме и IE отлаживается в страничке, на которой он выполняется:

В других браузерах с отладкой worker-ов пока что плохо.

Can I Use...


Поддержка разных worker-ов на Can I Use. Коротко, применительно к сегодняшнему вебу: worker есть на современных браузерах, sharedworker — на продвинутых десктопных браузерах, serviceworker — пока что рано.

...


Всё написанное актуально на лето 2015 года, не забывайте, что веб быстро меняется.

Ссылки


Using Web Workers (MDN)
The Basics of Web Workers
Living Standard: Web workers
Transferable Objects
How fast are web workers?
Tags:
Hubs:
+38
Comments 5
Comments Comments 5

Articles