JavaScript
Параллельное программирование на JavaScript с использованием Web Workers
JavaScript - один из самых популярных языков программирования. Это язык веб-браузера. Еще одна причина, по которой JavaScript стал настолько популярным, - это внедрение Node.js на стороне сервера.
Несмотря на то, что JavaScript - один из самых любимых языков, некоторые программисты не любят его. Одна из причин заключается в том, что JavaScript - это однопоточный язык. Это ужасно, когда дело касается неблокирующих операций.
Всякий раз, когда вы что-то делаете в JavaScript (например, вставляете текст внутри элемента DOM), вся веб-страница зависает и перестает отвечать.
Обычно мы этого не замечаем, потому что современные браузеры работают очень быстро. Но на протяжении всего жизненного цикла веб-страницы эта разница может стать весьма заметной.
Однопоточные языки выполняют только одно вычисление за раз.
Это означает, что когда ваш браузер запускает скрипт, все другие операции (например, манипуляции с DOM, анимация, рисование, отложенное выполнение и другие операции, которые происходят в основном потоке) будут остановлены.
Браузер просто не будет реагировать, пока выполнение этого скрипта не будет завершено.
К счастью, современные браузеры поставляются с веб-API, которые могут отложить выполнение сценария до тех пор, пока его выполнение не потребует среда выполнения.
Это происходит с помощью цикла событий. Все это однопоточное выполнение блестяще объясняется Филипом Робертсом в видео ниже. Вам обязательно стоит его посмотреть.
Что бы вы ни делали, от однопоточной природы JavaScript никуда не деться.
Давайте поговорим о том, как долго выполняющийся скрипт может блокировать веб-интерфейс или как он может замораживать вкладки браузера. Длительный сценарий может быть циклом for, который повторяется бесконечно или выполняет относительно большое количество итераций.
Просто для подтверждения запустим такой цикл for в консоли веб-браузера. В Google Chrome консоль JavaScript использует основной поток вкладки. Итак, если мы запустим такой цикл в консоли, он заморозит эту вкладку.
Ниже приведен пример продолжительного цикла for:
// benchmark test var start = Date.now(); // milliseconds var x = 0; for (var i = 0; i < 200000000; i++){ x = x + i; } console.log('ended in : ', -(start - Date.now())/1000, ' seconds'); // ended in : 9.867 seconds
В приведенном выше примере цикл for выполняет вычисления 200000000 раз (что очень много).
Если вы вставите приведенный выше код в консоль своего браузера и нажмете Enter, вы увидите, что вкладка не отвечает в течение длительного времени. Вы даже не можете закрыть вкладку! Вы вообще не сможете взаимодействовать с этой веб-страницей.
Но другие вкладки работают нормально, поскольку они выполняются в отдельных потоках или, возможно, в отдельных процессах.
В моем случае вышеуказанная программа выполняется примерно за десять секунд. Если в вашем случае он запускается немедленно, возможно, увеличьте количество итераций до 500000000 или больше. Это будет наш эталонный тест.
Некоторые люди советуют использовать функцию setTimeout
, чтобы обойти это поведение блокировки. Но вы можете попробовать на себе. Ни в коем случае не работает. Это означает, что выполнение однопоточного JavaScript может стать большой проблемой для нас, разработчиков.
Знакомство с веб-воркерами
По крайней мере, так было до тех пор, пока HTML5 не представил новый веб-API под названием Web Workers. Это было совместное усилие Mozilla Foundation и Google, направленное на то, чтобы сделать свои браузеры более мощными.
Позже многие другие браузеры переняли этот API. На сегодняшний день его поддерживают большинство основных браузеров.
Так что же такое веб-воркеры?
Веб-воркер - это программа JavaScript, работающая в другом потоке параллельно с основным потоком.
Браузер создает по одному потоку для каждой вкладки. Основной поток может порождать неограниченное количество веб-воркеров до тех пор, пока системные ресурсы пользователя не будут полностью израсходованы. Как только вкладка браузера закрывается, основной поток «умирает» и «убивает» всех созданных им веб-воркеров.
Программы JavaScript в основном потоке взаимодействуют с веб-воркерами с помощью событий. Эта связь происходит с использованием только события «сообщение» и полезной нагрузки данных.
Основной поток может «убить» созданных им веб-воркеров, а веб-воркер может «убить» себя. Когда веб-воркер умирает, связанные с ним потоки также умирают.
Но сначала, где мы можем использовать веб-воркеров?
Подумайте о самых неприятных вещах, с которыми вы сталкиваетесь при использовании веб-браузера. Большинство из нас ненавидят диалоговое окно «Страница не отвечает», которое появляется, когда выполнение какого-либо скрипта занимает много времени.
Это может быть хорошим вариантом использования для веб-работников. Перемещая ресурсоемкие задачи веб-воркерам, мы можем позволить им поднимать тяжелые грузы, в то время как наша веб-страница (основной поток) открыта для выполнения других важных задач.
Но, как правило, вы столкнетесь с проблемой «Страница не отвечает» из-за плохого дизайна.
Самое главное, что веб-воркеры должны использоваться в следующих случаях
- Кэширование данных и веб-страниц
- Обработка и кодирование изображений (преобразование base64)
- Рисование холста и фильтрация изображений
- Сетевой опрос и веб-сокеты
- Фоновые операции ввода-вывода
- Буферизация и анализ видео / аудио
- Виртуальная модель DOM
- Операции с локальной базой данных (indexedDB)
- Вычислительно-интенсивные операции с данными
Похоже, есть длинный список вещей, которые JavaScript не рекомендует делать в основном потоке. Но поверьте мне, когда вы начнете использовать веб-воркеров для таких задач, как указано выше, пользовательский опыт (UX) для вашего веб-приложения значительно улучшится.
Хватит разговоров, давайте перейдем к коду и реализуем нашего первого веб-воркера.
Из-за политик безопасности Google Chrome вы не сможете тестировать веб-работников по file://
протоколу. Вместо этого для запуска локального сервера лучше использовать модули npm http-server или live-server. Я объясню, как использовать live-сервер позже в этом блоге.
HTML5 предоставляет Worker
функцию-конструктор (или класс) для создания веб-воркеров. Он принимает строковый аргумент, который представляет собой путь к файлу JavaScript. Этот файл содержит код JavaScript для прослушивания события сообщения и выполнения задач.
Функция Worker
должна использоваться в сочетании с ключевым словом new
для создания экземпляра или объекта типа Worker. Следовательно, можно создать несколько экземпляров одного и того же скрипта веб-воркера.
<script> // inside -> index.html // resolved relative to index.html url path var worker = new Worker('worker.js'); ....
В приведенном выше коде мы создаем веб-воркера из файла worker.js
.
Здесь браузер попытается разрешить worker.js
относительно текущего пути страницы, на которой он был создан. Если вы находитесь в http://localhost
, то будет получено http://localhost/worker.js
. Если вы находитесь на http://localhost/app
, будет выведено http://localhost/app/worker.js
. Чтобы избежать этой проблемы, вы можете реализовать ее во внешнем файле JavaScript.
Если вышеуказанная реализация выполняется во внешнем файле JavaScript, например main.js
, то этот рабочий файл будет разрешен относительно него.
// inside -> main.js // resolved relative to main.js url path var worker = new Worker('worker.js');
Если файл, указанный в конструкторе Worker
, существует, браузер создаст рабочий поток, в котором этот файл будет загружен и запущен. Если при загрузке этого файла произошла ошибка, воркер завершится с ошибкой, и поток будет убит.
Давайте создадим простой проект для тестирования нашего первого веб-воркера. Ниже представлена структура проекта, которую мы собираемся использовать. Давайте создадим web-workers
папку, а под ней создадим пустые файлы.
web-workers |_ scripts |_ main.js // to hold script for index.html |_ workers |_ for.js // to hold web worker code |_ index.html // main page
Из приведенной выше структуры нашего приложения index.html
импортирует main.js
, который создаст веб-воркера с помощью for.js
скрипта.
Итак, index.html
будет выглядеть так:
<!-- index.html --> <!DOCTYPE html> <html> <head> <title>Web Workers</title> <!-- main.js --> <script src="./scripts/main.js"></script> </head> <body> <div id="result">loading...</div> </body> </html>
В index.html
мы импортируем только main.js
скрипт в разделе заголовка. Существует элемент div
с идентификатором result
, который мы будем использовать для печати результата веб-воркера. Вы немного увидите, как это работает.
В main.js
мы собираемся создать веб-воркера из for.js
скрипта. Поскольку мы говорили об относительном пути скрипта для веб-воркеров, путь for.js
относительно main.js
будет ../workers/for.js
, следовательно, main.js
будет выглядеть, как показано ниже.
Вы также можете использовать абсолютный путь /workers/for.js
, так как он также будет разрешен для файла main.js
, но обычно соблюдается соглашение об относительном пути.
// main.js var workerFor = new Worker('../workers/for.js'); // listen to message event of worker workerFor.addEventListener('message', function(event) { console.log('message received from workerFor => ', event.data); }); // listen to error event of worker workerFor.addEventListener('error', function(event) { console.error('error received from workerFor => ', event); });
Из приведенного выше кода мы создали веб-работника workerFor
из скрипта for.js
.
Веб-воркер генерирует два события, а именно. сообщение и ошибка на протяжении всего срока его службы. Мы можем прослушать это событие, используя метод addEventListener
на экземпляре worker.
Этот метод принимает два аргумента. Первый - это название события, которое мы слушаем. Вторая - это функция обратного вызова, которую мы хотим выполнить, когда это событие генерируется веб-воркером. Эта функция обратного вызова получает один аргумент - event
.
event
содержит множество свойств, одним из которых является data
для события message
. data
в событии - это фактические данные или полезная нагрузка, отправленные веб-работником. В случае события error
событие фактически является объектом ошибки JavaScript.
Событие message возникает, когда веб-воркер успешно отправляет данные, которые не являются объектом ошибки. Событие error возникает, когда исполнитель не зарегистрирован успешно или когда исполнитель отправляет объект ошибки в качестве полезной нагрузки.
Пока что мы поняли, как обращаться с веб-воркерами. Теперь давайте посмотрим, как работают веб-воркеры на практике. Ниже приведен очень простой пример веб-воркера.
Вскоре мы приступим к реализации цикла for внутри веб-воркеров, но очень полезно пошагово разбить работу с веб-воркерами, что поможет нам прояснить многие концепции, связанные с веб-воркерами.
// for.js self.postMessage('Hello World!');
В for.js
у нас есть странная переменная self
. self
- переменная контекста, предоставляемая средой выполнения JavaScript, которая относится к глобальному контексту. Я почти уверен, что вы никогда не использовали self
при доступе к глобальным переменным (скажем, x
) в JavaScript, потому что, когда переменная определяется глобально, вы просто используете имя переменной x
, а не window.x
, поскольку window
является глобальным контекстом, а все глобальные переменные доступно с синтаксисом window.varName
. self
в JavaScript также относится к глобальному контексту window
. Следовательно, любая глобальная переменная также может быть доступна с синтаксисом self.varName
.
В случае веб-воркеров self
используется для доступа к глобальной области, которая является самим воркером. Следовательно, если вы удалите self
из функции self.postMessage
, это тоже будет хорошо. postMessage
- это метод, предоставляемый исполнителем для генерации события message. Этот метод принимает один параметр, который является данными или полезной нагрузкой для передачи вместе с событием (который main.js будет доступен с event.data strong >).
Вы также можете использовать
this
вместоself
, но я не рекомендую это, потому что они сильно отличаются друг от друга. Вот объяснение https://stackoverflow.com/a/16876159/2790983.
Давайте добавим тайм-аут 5 секунд к вызову postMessage
, чтобы сделать этот пример более интересным. Код ниже вызовет функцию postMessage
через 5 секунд. Следовательно, в main.js мы должны получить это событие через 5 секунд.
setTimeout(() => { postMessage('Hello World!'); }, 5000);
Давайте также изменим main.js
для печати данных, полученных от веб-воркера, в #result
div. Это можно сделать с помощью простых манипуляций с DOM.
// modifications in main.js ... workerFor.addEventListener('message', function(event) { var div = document.getElementById('result'); div.innerHTML = 'message received => ' + event.data; }); ...
Приступим к применению. Но перед этим нам нужна программа, запускающая статический веб-сервер для размещения нашего проекта по протоколу http
. Для этого мы собираемся использовать модуль live-server
npm, который дает нам команды CLI для запуска статического веб-сервера из любого каталога. Чтобы установить live-server
с помощью npm, используйте команду ниже
$ npm install -g live-server
После завершения установки из папки нашего проекта используйте команду live-server
, которая должна запустить веб-сервер на 8080
порту. Он должен автоматически открывать вкладку браузера, но если этого не произошло, используйте URL http://localhost:8080
для перехода на index.html
страницу.
Каждый раз, когда вы вносите изменения в файлы, обслуживаемые live-server
, вам не нужно перезагружать веб-страницу, live-server
сделает это автоматически.
Вы увидите, что сначала на экране отображается loading...
, но через 5 секунд он меняется на message received => Hello World!
. У нас работает сервисный воркер.
Инструменты разработчика Chrome предоставляют большую поддержку веб-работникам. На вкладке «Источники» перейдите в раздел «Темы» на правой панели. Вы можете увидеть основной поток и поток for.js
. На левой панели вы можете увидеть for.js
веб-воркера и его местоположение. Используя эту панель, вы также можете отлаживать веб-воркеров, как обычный скрипт.
До сих пор мы видели только одностороннее общение, от веб-исполнителя for.js до main.js. Мы также можем общаться из main.js с веб-работником for.js, используя тот же postMessage
метод на экземпляре рабочего workerFor
в main.js. Поскольку postMessage
генерирует событие message, веб-исполнитель должен также прослушивать событие message аналогично тому, как это делается в main.js. Используя это, веб-воркер может понять, когда main.js запрашивает выполнение задачи.
Теперь давайте добавим наш ужасный цикл for внутри веб-воркера и вернем результат. Первоначально этот цикл for использовал блокировку вкладки нашего браузера. На этот раз этого не должно быть. Давайте создадим кнопку в index.html, чтобы генерировать событие message из main.js .
<!-- index.html --> <!DOCTYPE html> <html> <head> <title>Web Workers</title> <!-- main.js --> <script src="./scripts/main.js"></script> </head> <body> <div id="result">no results</div> <br/> <button onclick="loadResult()">Load Result</button> <!-- breaks for page scroll --> <br/><br/><br/><br/><br/><br/><br/><br/><br/><br/><br/> <br/><br/><br/><br/><br/><br/><br/><br/><br/><br/><br/> <br/><br/><br/><br/><br/><br/><br/><br/><br/><br/><br/> <br/><br/><br/><br/><br/><br/><br/><br/><br/><br/><br/> <br/><br/><br/><br/><br/><br/><br/><br/><br/><br/><br/> </body> </html>
В main.js мы добавим функцию loadResult
, которая будет отправлять сообщение о событии веб-воркеру.
// main.js var workerFor = new Worker('../workers/for.js'); // listen to message event of worker workerFor.onmessage = function(event){ var div = document.getElementById('result'); div.innerHTML = 'message received => ' + event.data; }; // listen to error event of worker workerFor.onerror = function(event) { console.error('error received from workerFor => ', event); var div = document.getElementById('result'); div.innerHTML = 'Error!'; }; // load results from web worker function loadResult() { // add loading text until `message` event listener replaces it var div = document.getElementById('result'); div.innerHTML = 'loading...'; // emit message event to worker workerFor.postMessage(null); // we don't need payload here };
elem.addEventListener(‘message’, callback)
эквивалентноelem.onmessage = callback
. Следовательно, вы можете решить, какой подход вы собираетесь использовать.
Поскольку при нажатии кнопки main.js отправляет событие сообщения в for.js, используя метод postMessage
на workerFor
, for.js должен его прослушать и запустить цикл for внутри функции обратного вызова. Сразу после кода цикла for мы должны вернуть результат обратно в main.js, используя тот же метод postMessage
. Это можно реализовать, как показано ниже.
// for.js self.onmessage = function(event) { var x = 0; for (var i = 0; i < 200000000; i++) { x = x + i; } self.postMessage(x); }
После сохранения этого файла live-reload
должен автоматически перезагрузить страницу. Теперь вы можете нажать кнопку и подождать 10 секунд, чтобы получить результат.
Чего ждать?
В рабочем потоке веб-приложения требуется меньше секунды, чтобы вычислить то же, что и в основном потоке почти 10 секунд. Вот почему веб-воркеры такие классные. Поскольку они работают в отдельном потоке, среда выполнения должна заботиться только об одной задаче, и она выполняет ее эффективно и быстро. Чтобы смоделировать большую вычислительную задержку, я собираюсь увеличить количество итераций for loop до 2000M. Посмотрим, как сейчас ведет себя наша страница.
Мы видим, что пока веб-воркер выполнял цикл for, наша страница или вкладка вообще не зависали. Таким образом, мы доказали, что веб-воркеры не блокируют.
Может быть один случай, когда вы закончили работу с веб-воркером и хотите его убить, скажем, чтобы освободить некоторые системные ресурсы (поток). Это можно сделать как в main.js, так и в for.js. В объекте веб-воркера есть метод terminate
, который убивает веб-воркера, или вы можете использовать метод self.close
изнутри веб-воркера, чтобы сделать то же самое. Как только веб-работа умирает, исчезает и поток, в котором она была создана. Когда вы пытаетесь отправить сообщение о событии мертвому веб-воркеру, сообщения не будут проходить, и это также не приведет к возникновению ошибок.
// modifications in for.js self.onmessage = function(event) { ... self.postMessage(x); self.close(); } ////////////////////// OR /////////////////////// // modifications inmain.js ... // listen to message event of worker workerFor.onmessage = function(event){ var div = document.getElementById('result'); div.innerHTML = 'message received => ' + event.data; workerFor.terminate(); }; ...
Итак, похоже, что мир веб-работников принадлежал нам 😀, но есть еще несколько вещей, которые нужно обсудить.
Передаваемые объекты
Когда мы отправляем полезную нагрузку по событию, используя событие postMessage
, мы клонируем полезную нагрузку из одного контекста в другой. Это также известно как передача по значению. Например, предположим, что у нас есть огромный файловый BLOB-объект в ArrayBuffer в основном потоке, который нам нужно обработать внутри веб-воркера. Когда мы отправляем этот большой двоичный объект в качестве полезной нагрузки, среда выполнения копирует эти данные, а затем отправляет их веб-воркеру, чтобы любые изменения, сделанные внутри веб-воркера, не повлияли на исходную копию. Это удобно, но у него есть существенные недостатки. Если эта полезная нагрузка, в данном случае большой двоичный объект изображения, имеет значительно больший размер, то создание ее копии займет много времени, и, поскольку это происходит в основном потоке, она заблокирует всю страницу и вкладку браузера.
Следовательно, нам нужна передача по ссылке, при которой копия не создается, а просто передается ссылка на данные (значение). Очевидно, что это проблематично и не является потокобезопасным.
Но JavaScript предлагает новый способ передачи данных между разными контекстами с помощью интерфейса Transferable. Любые типы, реализующие этот интерфейс, известные как переносимые объекты. Пока что этот интерфейс реализуют типы ArrayBuffer, MessagePort и ImageBitmap. Когда эти объекты переносятся в новый контекст, в нашем случае из основного потока в рабочий поток службы, их исходные копии очищаются, а их значения присваиваются полезной нагрузке события. После переноса эти объекты становятся стерилизованными или непригодными для использования и не могут быть перенесены снова.
Переносимые объекты в веб-воркерах передаются с использованием немного другого синтаксиса метода postMessage
.
worker.postMessage(payload, transferableObjects)
transferableObjects
в приведенном выше синтаксисе - это массив передаваемых объектов, который должен быть передан веб-исполнителю без создания его копий. transferableObjects
может быть [payload]
, если payload
- объект / массив не JavaScript, или значения / элементы объекта payload
, когда payload
- объект / массив JavaScript. Помните, что transferableObjects
всегда представляет собой массив передаваемых объектов.
Давайте посмотрим на простой пример.
Полезные данные события в случае Переданных объектов принимаются обычным образом. Приведенный выше пример выводит на консоль следующий результат.
Как вы можете видеть в строках [onmessage], буферы переданного массива пусты. Передача может происходить с обеих сторон, поскольку события могут отправляться как из main.js, так и из сервис-воркера.
Доступные объекты и API
Веб-воркер не имеет доступа ко всем API-интерфейсам браузера из-за его выполнения в другом потоке. Он не может получить доступ к DOM, потому что DOM не принадлежит его контексту. Объекты window
и document
также недоступны.
Но у него есть доступ только для чтения к объекту location
и полный доступ к объекту navigator
вместе с веб-API, такими как setTimeout
, setInterval
и application cache
.
Детские работники
Как мы говорили ранее, у веб-воркера есть возможность порождать других веб-воркеров и общаться с ними. Это следует тем же принципам, что и создание веб-воркеров из основного потока. Всякий раз, когда вы хотите создать новых дочерних веб-воркеров из родительского веб-воркера, представьте, что вы находитесь внутри основного потока и применяете те же принципы.
importScript ()
Мы можем импортировать любой внешний JavaScript внутри веб-воркера, используя функцию importScript, используя importScript(file.js, [...files])
. Эта функция доступна в глобальном контексте веб-воркера, поэтому вы также можете использовать self
для доступа к ней.
importScript('file1.js'); self.importScript('file2.js', 'file3.js');
Функция importScript загружает файлы JavaScript синхронно, поэтому веб-воркер блокируется до тех пор, пока файл не будет полностью загружен и запущен. После выполнения вы можете использовать код внутри этого файла, как будто он был встроен. Внешние файлы JavaScript могут использоваться для хранения некоторого общего кода, который может использоваться разными веб-воркерами.
Встроенные веб-воркеры
До сих пор мы видели код веб-воркера во внешнем файле JavaScript, но мы можем встроить код веб-воркера, создав URL-адрес большого двоичного объекта из кода JavaScript с помощью функции конструктора больших двоичных объектов. Затем этот URL-адрес большого двоичного объекта передается в функцию конструктора Worker. Это достигается, как показано ниже.
// create blob from JavaScript code (ES6 template literal) var blob = new Blob([` self.onmessage = function(e) { postMessage('msg from worker'); } `]); // create blob url from blob var blobURL = window.URL.createObjectURL(blob); // create web worker from blob url var worker = new Worker(blobURL); // send event to web worker worker.postMessage(null); // listen to message event from web worker worker.onmessage = function (event) { console.log(event.data); };
Веб-работники, о которых мы уже узнали, называются выделенными работниками. Это означает, что веб-воркер, порожденный основным потоком, не будет доступен другим основным потокам (из отдельных вкладок браузера). Но если вам нужна такая функциональность в веб-воркерах, Shared Workers здесь, чтобы помочь.
До сих пор мы использовали функцию конструктора Worker
, но для общих веб-воркеров мы использовали функцию конструктора SharedWorker
. Выделенные работники и общие работники более или менее работают одинаково, но в случае общих веб-работников они могут связываться и взаимодействовать с любым потоком, работающим в том же источнике (example.com). Я не собираюсь писать о Shared Workers, поскольку они реализованы не во всех браузерах, фактически, на данный момент только Chrome и Firefox поддерживают. Следовательно, нет смысла внедрять это в производство.
Но если вам интересно взглянуть на реализацию Shared Worker, посетите этот короткий блог по адресу https://www.sitepoint.com/javascript-shared-web-workers-html5/
Это был довольно большой блог о Рабочих. Но ждать!
Когда дело доходит до рабочих, нам не хватает еще одной вещи. Сервисные работники. Service worker - это основной компонент для разработки прогрессивных веб-приложений, и он очень мощный веб-воркер.