Worker-ы и shared worker-ы

    Во всех популярных языках есть потоки (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?
    • +38
    • 36,5k
    • 5
    Поделиться публикацией

    Похожие публикации

    AdBlock похитил этот баннер, но баннеры не зубы — отрастут

    Подробнее
    Реклама

    Комментарии 5

      +1
      Опера 12 рулит — я всегда это знал, но не знал, что она поддерживает и такую фичу.
      • НЛО прилетело и опубликовало эту надпись здесь
          0
          Вы о websocket? Они поддерживаются.
          • НЛО прилетело и опубликовало эту надпись здесь
              0
              Проверял, даже использовал. В Firefox сделали в этом году, в остальных была уже раньше.

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

        Самое читаемое