Проблема: параллелизм JavaScript
Существует ряд узких мест, препятствующих переносу интересных приложений (скажем, из серверных реализаций) на клиентский JavaScript. Некоторые из них включают совместимость браузера, статическую типизацию, доступность и производительность. К счастью, последнее быстро уходит в прошлое, поскольку производители браузеров быстро улучшают скорость своих движков JavaScript.
Единственное, что остается помехой для JavaScript, — это сам язык. JavaScript — это однопоточная среда, то есть несколько сценариев не могут выполняться одновременно. В качестве примера представьте себе сайт, которому необходимо обрабатывать события пользовательского интерфейса, запрашивать и обрабатывать большие объемы данных API, а также манипулировать DOM. Довольно распространенное явление, не так ли? К сожалению, все это не может происходить одновременно из-за ограничений среды выполнения JavaScript в браузерах. Выполнение скрипта происходит в одном потоке.
Разработчики имитируют «параллелизм», используя такие методы, как setTimeout()
, setInterval()
, XMLHttpRequest
и обработчики событий. Да, все эти функции работают асинхронно, но отсутствие блокировки не обязательно означает параллелизм. Асинхронные события обрабатываются после завершения текущего исполняемого сценария. Хорошая новость в том, что HTML5 дает нам нечто лучшее, чем эти хаки!
Знакомство с веб-воркерами: добавьте многопоточность в JavaScript
Спецификация Web Workers определяет API для создания фоновых сценариев в вашем веб-приложении. Веб-воркеры позволяют вам выполнять такие действия, как запуск длительных сценариев для выполнения ресурсоемких задач, но без блокировки пользовательского интерфейса или других сценариев для обработки взаимодействия с пользователем. Они помогут положить конец этому неприятному диалогу «неотвечающий сценарий», который мы все полюбили:
Работники используют потоковую передачу сообщений для достижения параллелизма. Они идеально подходят для поддержания обновления вашего пользовательского интерфейса, повышения производительности и отзывчивости для пользователей.
Типы веб-работников
Стоит отметить, что в спецификации обсуждаются два типа веб-воркеров: выделенные рабочие и общие рабочие . В этой статье речь пойдет только о преданных своему делу работниках. Я буду называть их «веб-работниками» или «работниками».
Начиная
Веб-воркеры выполняются в изолированном потоке. В результате код, который они выполняют, необходимо содержать в отдельном файле. Но прежде чем мы это сделаем, первое, что нужно сделать, — это создать новый объект Worker
на вашей главной странице. Конструктор принимает имя рабочего скрипта:
var worker = new Worker('task.js');
Если указанный файл существует, браузер создаст новый рабочий поток, который загружается асинхронно. Рабочий не запустится, пока файл не будет полностью загружен и выполнен. Если путь к вашему работнику возвращает 404, работник автоматически выйдет из строя.
После создания работника запустите его, вызвав метод postMessage()
:
worker.postMessage(); // Start the worker.
Общение с работником посредством передачи сообщений
Связь между произведением и его родительской страницей осуществляется с помощью модели событий и метода postMessage()
. В зависимости от вашего браузера/версии postMessage()
может принимать в качестве единственного аргумента либо строку, либо объект JSON. Последние версии современных браузеров поддерживают передачу объекта JSON.
Ниже приведен пример использования строки для передачи «Hello World» работнику в doWork.js. Рабочий просто возвращает переданное ему сообщение.
Основной сценарий:
var worker = new Worker('doWork.js');
worker.addEventListener('message', function(e) {
console.log('Worker said: ', e.data);
}, false);
worker.postMessage('Hello World'); // Send data to our worker.
doWork.js (работник):
self.addEventListener('message', function(e) {
self.postMessage(e.data);
}, false);
Когда postMessage()
вызывается с главной страницы, наш работник обрабатывает это сообщение, определяя обработчик onmessage
для события message
. Полезная нагрузка сообщения (в данном случае «Hello World») доступна в Event.data
. Хотя этот конкретный пример не очень интересен, он демонстрирует, что postMessage()
также является средством передачи данных обратно в основной поток. Удобный!
Сообщения, передаваемые между главной страницей и рабочими процессами, копируются, а не передаются. Например, в следующем примере свойство msg сообщения JSON доступно в обоих местах. Похоже, что объект передается непосредственно работнику, хотя он выполняется в отдельном выделенном пространстве. На самом деле происходит следующее: объект сериализуется при передаче работнику, а затем десериализуется на другом конце. Страница и рабочий процесс не используют один и тот же экземпляр, поэтому в конечном результате при каждом проходе создается дубликат. Большинство браузеров реализуют эту функцию путем автоматического кодирования/декодирования значения JSON на обоих концах.
Ниже приведен более сложный пример передачи сообщений с использованием объектов JSON.
Основной сценарий:
<button onclick="sayHI()">Say HI</button>
<button onclick="unknownCmd()">Send unknown command</button>
<button onclick="stop()">Stop worker</button>
<output id="result"></output>
<script>
function sayHI() {
worker.postMessage({'cmd': 'start', 'msg': 'Hi'});
}
function stop() {
// worker.terminate() from this script would also stop the worker.
worker.postMessage({'cmd': 'stop', 'msg': 'Bye'});
}
function unknownCmd() {
worker.postMessage({'cmd': 'foobard', 'msg': '???'});
}
var worker = new Worker('doWork2.js');
worker.addEventListener('message', function(e) {
document.getElementById('result').textContent = e.data;
}, false);
</script>
doWork2.js:
self.addEventListener('message', function(e) {
var data = e.data;
switch (data.cmd) {
case 'start':
self.postMessage('WORKER STARTED: ' + data.msg);
break;
case 'stop':
self.postMessage('WORKER STOPPED: ' + data.msg +
'. (buttons will no longer work)');
self.close(); // Terminates the worker.
break;
default:
self.postMessage('Unknown command: ' + data.msg);
};
}, false);
Передаваемые объекты
Большинство браузеров реализуют алгоритм структурированного клонирования , который позволяет передавать в или из рабочих процессов более сложные типы, такие как объекты File
, Blob
, ArrayBuffer
и JSON. Однако при передаче этих типов данных с помощью postMessage()
копия все равно создается. Поэтому, если вы передаете большой файл размером 50 МБ (например), при передаче этого файла между рабочим и основным потоком возникают заметные накладные расходы.
Структурированное клонирование — это здорово, но копирование может занять сотни миллисекунд. Для борьбы с ударом по производительности можно использовать Transferable Objects .
С помощью Transferable Objects данные передаются из одного контекста в другой. Это нулевое копирование, что значительно повышает производительность отправки данных работнику. Если вы из мира C/C++, думайте об этом как о передаче по ссылке. Однако, в отличие от передачи по ссылке, «версия» из контекста вызова больше не доступна после передачи в новый контекст. Например, при переносе ArrayBuffer из основного приложения в Worker исходный ArrayBuffer
очищается и больше не пригоден для использования. Его содержимое (в буквальном смысле слова) переносится в контекст Worker.
Чтобы использовать передаваемые объекты, используйте немного другую сигнатуру postMessage()
:
worker.postMessage(arrayBuffer, [arrayBuffer]);
window.postMessage(arrayBuffer, targetOrigin, [arrayBuffer]);
Рабочий случай: первый аргумент — это данные, а второй — список элементов, которые следует передать. Кстати, первый аргумент не обязательно должен быть ArrayBuffer
. Например, это может быть объект JSON:
worker.postMessage({data: int8View, moreData: anotherBuffer},
[int8View.buffer, anotherBuffer]);
Важным моментом является то, что второй аргумент должен быть массивом ArrayBuffer
. Это ваш список передаваемых предметов.
Дополнительную информацию о передаваемых объектах можно найти в нашей публикации на сайте Developer.chrome.com .
Рабочая среда
Область действия работника
В контексте работника и self
, и this
ссылаются на глобальную область действия работника. Таким образом, предыдущий пример также можно записать так:
addEventListener('message', function(e) {
var data = e.data;
switch (data.cmd) {
case 'start':
postMessage('WORKER STARTED: ' + data.msg);
break;
case 'stop':
...
}, false);
Альтернативно, вы можете установить обработчик событий onmessage
напрямую (хотя addEventListener
всегда поощряется ниндзя JavaScript).
onmessage = function(e) {
var data = e.data;
...
};
Функции, доступные работникам
Из-за своего многопоточного поведения Web Workers имеет доступ только к подмножеству функций JavaScript:
- Объект
navigator
- Объект
location
(только для чтения) -
XMLHttpRequest
-
setTimeout()/clearTimeout()
иsetInterval()/clearInterval()
- Кэш приложения
- Импорт внешних скриптов с помощью метода
importScripts()
- Создание других веб-работников
Работники НЕ имеют доступа к:
- DOM (он не потокобезопасен)
- Объект
window
- Объект
document
-
parent
объект
Загрузка внешних скриптов
Вы можете загрузить внешние файлы скриптов или библиотеки в рабочий процесс с помощью функции importScripts()
. Этот метод принимает ноль или более строк, представляющих имена файлов для импортируемых ресурсов.
В этом примере скрипты script1.js
и script2.js
загружаются в рабочий процесс:
рабочий.js:
importScripts('script1.js');
importScripts('script2.js');
Это также можно записать как один оператор импорта:
importScripts('script1.js', 'script2.js');
Подработники
Рабочие имеют возможность порождать детей-работников. Это отлично подходит для дальнейшего разделения больших задач во время выполнения. Однако у субработников есть несколько предостережений:
- Подработчики должны размещаться в том же источнике, что и родительская страница.
- URI внутри подчиненных рабочих процессов разрешаются относительно местоположения их родительского рабочего процесса (в отличие от главной страницы).
Имейте в виду, что большинство браузеров создают отдельные процессы для каждого работника. Прежде чем приступить к созданию рабочей фермы, будьте осторожны, чтобы не захватить слишком много системных ресурсов пользователя. Одна из причин этого заключается в том, что сообщения, передаваемые между главными страницами и рабочими процессами, копируются, а не передаются. См. раздел «Общение с работником посредством передачи сообщений».
Пример создания подчиненного работника см . в спецификации.
Линейные рабочие
Что, если вы хотите создать рабочий скрипт «на лету» или создать автономную страницу без необходимости создавать отдельные рабочие файлы? С помощью Blob()
вы можете «встроить» своего работника в тот же HTML-файл, что и основную логику, создав дескриптор URL-адреса кода работника в виде строки:
var blob = new Blob([
"onmessage = function(e) { postMessage('msg from worker'); }"]);
// Obtain a blob URL reference to our worker 'file'.
var blobURL = window.URL.createObjectURL(blob);
var worker = new Worker(blobURL);
worker.onmessage = function(e) {
// e.data == 'msg from worker'
};
worker.postMessage(); // Start the worker.
URL-адреса BLOB-объектов
Волшебство приходит с вызовом window.URL.createObjectURL()
. Этот метод создает простую строку URL-адреса, которую можно использовать для ссылки на данные, хранящиеся в File
DOM или объекте Blob
. Например:
blob:http://localhost/c745ef73-ece9-46da-8f66-ebes574789b1
URL-адреса BLOB-объектов уникальны и действуют в течение всего времени существования вашего приложения (например, до тех пор, пока document
не будет выгружен). Если вы создаете много URL-адресов BLOB-объектов, рекомендуется освободить ссылки, которые больше не нужны. Вы можете явно освободить URL-адреса Blob, передав их в window.URL.revokeObjectURL()
:
window.URL.revokeObjectURL(blobURL);
В Chrome есть удобная страница для просмотра всех URL-адресов созданных больших двоичных объектов: chrome://blob-internals/
.
Полный пример
Сделав еще один шаг вперед, мы можем разобраться в том, как JS-код воркера встроен в нашу страницу. Этот метод использует тег <script>
для определения работника:
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
</head>
<body>
<div id="log"></div>
<script id="worker1" type="javascript/worker">
// This script won't be parsed by JS engines
// because its type is javascript/worker.
self.onmessage = function(e) {
self.postMessage('msg from worker');
};
// Rest of your worker code goes here.
</script>
<script>
function log(msg) {
// Use a fragment: browser will only render/reflow once.
var fragment = document.createDocumentFragment();
fragment.appendChild(document.createTextNode(msg));
fragment.appendChild(document.createElement('br'));
document.querySelector("#log").appendChild(fragment);
}
var blob = new Blob([document.querySelector('#worker1').textContent]);
var worker = new Worker(window.URL.createObjectURL(blob));
worker.onmessage = function(e) {
log("Received: " + e.data);
}
worker.postMessage(); // Start the worker.
</script>
</body>
</html>
На мой взгляд, этот новый подход немного чище и понятнее. Он определяет тег сценария с id="worker1"
и type='javascript/worker'
(поэтому браузер не анализирует JS). Этот код извлекается в виде строки с помощью document.querySelector('#worker1').textContent
и передается в Blob()
для создания файла.
Загрузка внешних скриптов
При использовании этих методов для встраивания рабочего кода importScripts()
будет работать только в том случае, если вы укажете абсолютный URI. Если вы попытаетесь передать относительный URI, браузер сообщит об ошибке безопасности. Причина в том, что рабочий процесс (теперь созданный из URL-адреса большого двоичного объекта) будет разрешен с помощью префикса blob:
в то время как ваше приложение будет работать по другой схеме (предположительно http://
). Следовательно, сбой будет вызван ограничениями перекрестного происхождения.
Один из способов использования importScripts()
во встроенном рабочем процессе — «внедрить» текущий URL-адрес вашего основного сценария, передав его встроенному рабочему процессу и создав абсолютный URL-адрес вручную. Это гарантирует, что внешний скрипт будет импортирован из того же источника. Предполагая, что ваше основное приложение работает с http://example.com/index.html
:
...
<script id="worker2" type="javascript/worker">
self.onmessage = function(e) {
var data = e.data;
if (data.url) {
var url = data.url.href;
var index = url.indexOf('index.html');
if (index != -1) {
url = url.substring(0, index);
}
importScripts(url + 'engine.js');
}
...
};
</script>
<script>
var worker = new Worker(window.URL.createObjectURL(bb.getBlob()));
worker.postMessage(<b>{url: document.location}</b>);
</script>
обработка ошибок
Как и в случае с любой логикой JavaScript, вам потребуется обрабатывать любые ошибки, возникающие в ваших веб-воркерах. Если во время выполнения работника возникает ошибка, запускается событие ErrorEvent
. Интерфейс содержит три полезных свойства, позволяющих выяснить, что пошло не так: filename
— имя рабочего сценария, вызвавшего ошибку, lineno
— номер строки, в которой произошла ошибка, и message
— осмысленное описание ошибки. Вот пример настройки обработчика события onerror
для печати свойств ошибки:
<output id="error" style="color: red;"></output>
<output id="result"></output>
<script>
function onError(e) {
document.getElementById('error').textContent = [
'ERROR: Line ', e.lineno, ' in ', e.filename, ': ', e.message
].join('');
}
function onMsg(e) {
document.getElementById('result').textContent = e.data;
}
var worker = new Worker('workerWithError.js');
worker.addEventListener('message', onMsg, false);
worker.addEventListener('error', onError, false);
worker.postMessage(); // Start worker without a message.
</script>
Пример : workerWithError.js пытается выполнить 1/x, где x не определен.
// TODO: DevSite – удален пример кода, поскольку в нем использовались встроенные обработчики событий
рабочийСError.js:
self.addEventListener('message', function(e) {
postMessage(1/x); // Intentional error.
};
Несколько слов о безопасности
Ограничения при локальном доступе
Из-за ограничений безопасности Google Chrome рабочие процессы не будут запускаться локально (например, из file://
) в последних версиях браузера. Вместо этого они терпят неудачу молча! Чтобы запустить приложение из схемы file://
, запустите Chrome с установленным флагом --allow-file-access-from-files
.
Другие браузеры не налагают такого же ограничения.
Соображения об одном и том же происхождении
Рабочие скрипты должны быть внешними файлами с той же схемой, что и их вызывающая страница. Таким образом, вы не можете загрузить сценарий из URL-адреса data:
или javascript:
URL, а страница https:
не может запускать рабочие сценарии, начинающиеся с URL-адресов http:
Случаи использования
Так какое же приложение будет использовать веб-работников? Вот еще несколько идей, которые разбудят ваш мозг:
- Предварительная выборка и/или кэширование данных для последующего использования.
- Подсветка синтаксиса кода или другое форматирование текста в реальном времени.
- Программа проверки орфографии.
- Анализ видео или аудио данных.
- Фоновый ввод-вывод или опрос веб-сервисов.
- Обработка больших массивов или огромных ответов JSON.
- Фильтрация изображений в
<canvas>
. - Обновление многих строк локальной веб-базы данных.
Дополнительные сведения о вариантах использования API Web Workers см. на странице Обзор Workers .
Демо
Рекомендации
- Спецификация веб-работников
- «Использование веб-работников» из веб-документов Mozilla Developer Network.
- «Веб-работники восстают!» из Дев.Оперы