Основные сведения об объектах Web Worker

HTML5 Rocks

Проблема: параллельное выполнение кода JavaScript

Есть несколько моментов, не позволяющих переносить интересные JavaScript-программы на сторону клиента (например, для разгрузки серверов). К ним относятся совместимость браузеров, статическая типизация, доступность и эффективность. К счастью, последняя проблема быстро отходит в прошлое, поскольку разработчики браузеров постоянно повышают скорость функционирования платформ JavaScript.

Одной из проблем, связанных с JavaScript, остается сам язык. JavaScript – это однопоточная среда, в которой несколько скриптов не могут выполняться одновременно. Например, представим сайт, на котором необходимо управлять событиями интерфейса пользователя, запрашивать и обрабатывать большие объемы данных API и работать с моделью DOM. Очень знакомо, верно? К сожалению, все эти действия не могут выполняться одновременно из-за ограничений в среде выполнения JavaScript браузеров. Скрипты выполняются в пределах одного потока.

Разработчики имитируют параллельное выполнение кода с помощью таких средств, как методы setTimeout() и setInterval(), технология XMLHttpRequest, а также обработчики событий. Все эти функции выполняются асинхронно, но отсутствие блокировки не обязательно означает параллельное выполнение. Асинхронные события обрабатываются после возврата из текущего выполняющегося скрипта. Приятным моментом является тот факт, что в HTML5 есть более удобный способ реализации параллельного выполнения.

Знакомство с объектами Web Worker: многопоточность в JavaScript

Спецификация объектов Web Worker определяет API для создания фоновых скриптов в веб-приложениях. Эти объекты позволяют запускать долговременные скрипты для выполнения задач, требующих большого объема вычислений, не прибегая к блокировке интерфейса пользователя или других скриптов, управляющих взаимодействием пользователя с системой. Они помогают избавиться от надоевшего диалогового окна "Скрипт не отвечает", к которому все уже успели привыкнуть.

Диалоговое окно неотвечающего скрипта
Стандартное диалоговое окно неотвечающего скрипта

Объекты Web Worker используют потоковую передачу сообщений для реализации параллельности. Они идеально подходят для обновления интерфейса, обеспечения его эффективности и реализации оперативного взаимодействия с пользователями.

Типы объектов Web Worker

Следует заметить, что в спецификации определяются два типа объектов Web Worker: выделенные и общие. В этой статье рассматриваются только выделенные объекты Web Worker (мы будем называть их объектами Web Worker или объектами Worker).

Начало работы

Объекты Web Worker запускаются в изолированном потоке. По этой причине выполняемый ими код должен храниться в отдельном файле. Однако сначала необходимо создать новый объект Worker на главной странице. Конструктор принимает название скрипта объекта:

var worker = new Worker('task.js');

Если указанный файл существует, браузер создаст новый поток объекта Worker, загружаемый асинхронно. Объект не запускается до полной загрузки и выполнения файла. Если путь к объекту Worker возвращает ошибку 404, его выполнение прекращается без уведомлений.

После создания объекта Worker его можно запустить с помощью метода postMessage(), как показано ниже.

worker.postMessage(); // Start the worker.

Взаимодействие с объектами Worker путем обмена сообщениями

Взаимодействие объекта Worker с исходной страницей осуществляется с помощью модели событий и метода postMessage(). В зависимости от типа и версии браузера метод postMessage() принимает строку или объект JSON в качестве единственного аргумента. Новейшие версии современных браузеров поддерживают передачу объектов JSON.

Ниже приведен пример использования строки для передачи сообщения Hello World объекту Worker в скрипте doWork.js. Объект Worker просто возвращает полученное сообщение.

Основной скрипт:

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 (объект Worker)

self.addEventListener('message', function(e) {
  self.postMessage(e.data);
}, false);

Если метод postMessage() вызывается на главной странице, объект Worker обрабатывает сообщение, определяя обработчик onmessage для события message. Информационное наполнение сообщения (в данном случае – Hello World) доступно в объекте Event.data. Хотя этот конкретный пример и не является интересным, он демонстрирует, что метод postMessage() также служит для передачи данных обратно в главный поток. Удобство использования

Сообщения, передаваемые между главной страницей и объектами Worker, копируются, но общий доступ к ним не предоставляется. Так, в следующем примере свойство msg сообщения JSON доступно в обоих местах. Может показаться, что элемент передается непосредственно объекту Worker, хотя действие выполняется в выделенном пространстве. В действительности при передаче в объект Worker элемент преобразуется в последовательную форму, а обратное преобразование затем происходит в конце пути. Страница и объект Worker не имеют общего доступа к одному экземпляру элемента, и в результате каждый раз при передаче создается его дубликат. Большинство браузеров реализуют эту функцию путем автоматической кодировки и декодирования значения в конечных точках маршрута, используя для этого технологию 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() {
    // Calling 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);

Примечание. Прекратить работу объекта Worker можно двумя способами: вызвать метод worker.terminate() на главной странице или self.close() в самом объекте.

Пример. Запустите этот объект Worker.

Среда объекта Worker

Область действия объекта Worker

В контексте объекта Worker и 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 (хотя разработчики JavaScript всегда предлагают использовать addEventListener).

onmessage = function(e) {
  var data = e.data;
  ...
};

Функции, доступные для объектов Web Worker

В связи со своим многопоточным характером объекты Web Worker имеют доступ только к определенному набору функций JavaScript, указанных ниже.

Объекты Web Worker не имеют доступа к перечисленным ниже возможностям и элементам.

  • Модель DOM (она не ориентирована на многопоточное исполнение)
  • Объект window
  • Объект document
  • Объект parent

Загрузка внешних скриптов

С помощью функции importScripts() можно загружать в объект Worker внешние файлы скриптов и библиотек. Этот метод принимает ноль или более строк, представляющих названия файлов для импорта ресурсов.

В этом примере в объект загружаются файлы script1.js и script2.js.

worker.js:

importScripts('script1.js');
importScripts('script2.js');

Это также можно записать в виде единого оператора импорта:

importScripts('script1.js', 'script2.js');

Субобъекты Worker

Объекты Worker могут создавать подчиненные элементы. Они являются удобным инструментом для дальнейшего распределения больших задач на этапе выполнения. Однако субобъекты Worker можно использовать с некоторыми оговорками.

  • Субобъекты Worker следует размещать в пределах того же источника, где располагается исходная страница.
  • В контексте субобъектов URI связаны с расположением исходного объекта Worker (в отличие от главной страницы).

Следует помнить о том, что большинство браузеров создают отдельные процессы для каждого объекта Worker. Прежде чем создавать субобъекты, необходимо проследить за тем, чтобы связанные с ними процессы разумно использовали ресурсы пользовательской системы. Их нерациональное применение может быть связано с копированием сообщений, передаваемых между главными страницами и объектами Worker, вместо их совместного использования. Подробнее об этом можно узнать в разделе Взаимодействие с объектами Worker путем обмена сообщениями.

Чтобы научиться создавать субобъекты Worker, ознакомьтесь с этим примером в спецификации.

Встраиваемые объекты Web Worker

Как поступить в ситуации, когда необходимо сформировать скрипт объекта Web Worker в процессе работы или создать автономную страницу, не использующую отдельные файлы объектов? С новым интерфейсом BlobBuilder объекты Web Worker можно "встраивать" в тот же HTML-файл, в котором содержится основной логический блок. Для этого создаются объекты BlobBuilder, в которые код объектов Worker добавляется в качестве строки, как показано ниже.

// Prefixed in Webkit, Chrome 12, and FF6: window.WebKitBlobBuilder, window.MozBlobBuilder
var bb = new BlobBuilder();
bb.append("onmessage = function(e) { postMessage('msg from worker'); }");

// Obtain a blob URL reference to our worker 'file'.
// Note: window.webkitURL.createObjectURL() in Chrome 10+.
var blobURL = window.URL.createObjectURL(bb.getBlob());

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 или Blob модели DOM. Например:

blob:http://localhost/c745ef73-ece9-46da-8f66-ebes574789b1

URL объектов Blob уникальны и сохраняются на протяжении всего времени выполнения программы (например, до выгрузки из памяти объекта document). Если необходимо создать много URL объектов Blob, рекомендуется удалять ненужные ссылки. Сделать это можно путем их передачи методу window.URL.revokeObjectURL(), как показано ниже.

window.URL.revokeObjectURL(blobURL); // window.webkitURL.createObjectURL() in Chrome 10+.

В браузере Chrome есть удобная страница для просмотра всех созданных URL объектов Blob: chrome://blob-internals/.

Полный пример

Теперь рассмотрим, как JavaScript-код объекта Worker встраивается в страницу. Для этого используется тег <script>, определяющий объект Worker.

<!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 bb = new BlobBuilder();
    bb.append(document.querySelector('#worker1').textContent);

    // Note: window.webkitURL.createObjectURL() in Chrome 10+.
    var worker = new Worker(window.URL.createObjectURL(bb.getBlob()));
    worker.onmessage = function(e) {
      log("Received: " + e.data);
    }
    worker.postMessage(); // Start the worker.
  </script>
</body>
</html>

Такой подход представляется мне более понятным и правильным. Тег скрипта задается с параметрами id="worker1" и type='javascript/worker' (поэтому браузер не выполняет анализ JavaScript-кода). Этот код извлекается в виде строки с помощью метода document.querySelector('#worker1').textContent и передается в функцию BlobBuilder.append().

Загрузка внешних скриптов

При встраивании кода объекта Worker таким образом метод importScripts() принимает только абсолютные URI. Если попытаться передать ему относительный URI, браузер сообщит об ошибке системы безопасности. Это связано с тем, что объект Worker (теперь создаваемый с использованием URL элемента Blob) разрешается с префиксом blob:, в то время как программа выполняется в другой схеме (как правило, http://). Таким образом, ошибка возникает из-за ограничений, связанных с разным происхождением.

Один из способов применения метода importScripts() во встроенном объекте Worker – вставка текущего URL основного скрипта путем его передачи объекту Worker и создания абсолютного 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({url: document.location});
</script>

Обработка ошибок

Как и любой логический блок JavaScript, объекты Web Worker требуют обработки любых возникающих ошибок. Если ошибка возникает при выполнении объекта, срабатывает событие ErrorEvent. Интерфейс содержит три полезных свойства для определения причины ошибки: filename – название скрипта объекта Worker, вызвавшего ошибку, 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 не указано.

workerWithError.js:

self.addEventListener('message', function(e) {
  postMessage(1/x); // Intentional error.
};

Немного о безопасности

Ограничение локального доступа

В связи с ограничениями безопасности в последних версиях браузера Google Chrome объекты Worker не запускаются локально (например, по адресу file://). Вместо этого они прекращают работу без какого-либо уведомления. Для выполнения программы в схеме file:// необходимо запустить Chrome с установленной пометкой --allow-file-access-from-files. ПРИМЕЧАНИЕ. Не рекомендуется запускать основной браузер с такой пометкой. Ее следует использовать только для проверки, а не в режиме обычного просмотра страниц.

Это ограничение не относится к другим браузерам.

Рекомендации по использованию одного источника

Скрипты объектов Worker должны представлять собой внешние файлы с той же схемой, что и схема страницы вызова. Поэтому невозможно загрузить скрипт из URL data: или javascript:, а со страницы https: не запускаются скрипты объектов Worker, начинающиеся с URL http:.

Примеры использования

Какие виды программ используют объекты Web Worker? К сожалению, эти объекты являются относительно новой технологией, и в большинстве существующих примеров и руководств представлена обработка простых чисел. Хотя это не самое захватывающее занятие, оно полезно для понимания принципов работы объектов Web Worker. Ниже приведено несколько идей, о реализации которых можно подумать.

  • Предварительный выбор или кэширование данных для дальнейшего использования.
  • Синтаксическое выделение кода или другое форматирование текста в реальном времени.
  • Проверка правописания.
  • Анализ видео- и аудиоданных.
  • Фоновые операции ввода-вывода и опрос веб-служб.
  • Обработка больших массивов данных и объемных ответов JSON.
  • Фильтрация изображений на элементе <canvas>.
  • Обновление множества строк в локальной веб-базе данных.

Демонстрационные примеры

Ссылки

Comments

0