Знакомство с Web Workers
Дата публикации: 23.04.2012
Web Workers — технология, позволяющая выполнять Javascript в фоновом режиме, что в свою очередь позволяет воспользоваться преимуществами многоядерных архитектур.
Поддержка браузерами Web Workers
Где использовать
- самое очевидное — для сложных, объемных вычислений;
- обработка/анализ изображений и видео;
- разбор объемных данных полученных после вызова XMLHTTPRequest;
- анализ вводимого текста пользователем «на лету», не блокируя интерфейс;
- запросы к локальным базам данных.
Вообще некоторые утверждают, что применение Web Workers практически ограничивается фантазией разработчика несмотря на ограничения технологии. Нужно помнить, что вызов фонового обработчика, передача ему и получение от него данных занимает какое-то время. Поэтому использование Web Workers должно быть обдуманным.
Возможности и ограничения
Web Workers не имеет доступа к:
- DOM;
- объектам window, document, parent.
При этом доступно:
- XMLHttpRequest;
- объекты navigator, location;
- setTimeout()/clearTimeout() и setInterval()/clearInterval();
- внедрение внешних скриптов с помощью метода importScripts();
- порождение субворкеров;
- Application Cache.
Пример
Целью этого примера является базовое знакомство с Web Workers, чтобы можно было начать использовать не тратить очень много времени на изучение спецификации.
Для примера будем рисовать на канве круг, вычисление позиций которого посчитаем тяжелой операцией. Вычисления выносим в отдельный js файл, чтобы появилась возможность использовать фоновые вычисления. Основной код:
window.onload = function()
{
var canvas = document.getElementById("scene"), // канва
ctx = canvas.getContext("2d"),
canvasW = canvas.offsetWidth,
canvasH = canvas.offsetHeight,
posY,
posX = 0, // начальная позиция
angle = 0, // начальный угол
r = 30;
/* цвета и стили фигуры */
ctx.lineWidth = 2;
ctx.fillStyle = "green";
ctx.strokeStyle = "black";
/* если поддерживается web workers используем параллельные вычисления */
if(!!window.Worker)
{
/*
создаем worker
в качестве параметра указываем js файл и путь к нему, где будут проводиться тяжелые операции
*/
var worker = new Worker("worker.js");
/*
делаем запрос чтобы получить первые результаты
передаем необходимые данные для вычислений
данные можно передавать переменными/константами. Если данных несколько - формат JSON
*/
worker.postMessage({
posX: posX,
angle: angle,
r: r,
canvasW: canvasW,
canvasH: canvasH
});
/* render для поддерживающих web worker */
function updateWorker()
{
/*
получаем результаты
обработчик сработает как только вычисления завершаться
*/
worker.onmessage = function (event) {
var data = event.data;
posX = data.posX,
posY = data.posY,
angle = data.angle;
};
ctx.clearRect(0, 0, 600, 400);
/* рисуем */
ctx.beginPath();
ctx.arc(posX, posY, r, 0, Math.PI*2, false);
ctx.closePath();
ctx.stroke();
ctx.fill();
/* отправляем новый запрос */
worker.postMessage({
posX: posX,
angle: angle,
r: r,
canvasW: canvasW,
canvasH: canvasH
});
requestAnimFrame(updateWorker);
};
requestAnimFrame(updateWorker); // запускаем рендер
}
/* если web workers не поддерживается - по старинке: один поток */
else
{
/* render для не поддерживающих web workers */
function update()
{
ctx.clearRect(0, 0, 600, 400);
/* рисуем */
ctx.beginPath();
ctx.arc(posX, posY, r, 0, Math.PI*2, false);
ctx.closePath();
ctx.stroke();
ctx.fill();
// вычисления
posY = canvasH/2 + Math.sin(angle) * 50;
angle += 0.05;
posX+=0.5;
if(posX>canvasW+r) posX=-r;
requestAnimFrame(update);
}
requestAnimFrame(update);
}
}
Код воркера (worker.js):
/* начинаем вычиления как только пришли исходные данные */
onmessage = function(event) {
var data = event.data,
canvasH = data.canvasH,
angle = data.angle,
posX = data.posX,
canvasW = data.canvasW,
r = data.r;
// вычисления
posY = canvasH/2 + Math.sin(angle) * 50;
angle += 0.05;
posX+=0.5;
if(posX>canvasW+r) posX=-r;
// отправляем назад результаты
postMessage({
posX: posX,
posY: posY,
angle: angle
});
}
В живую. Данный вариант далек от оптимального, т.к. отправка/получение данных выходят ощутимо тяжелее, чем расчеты. Приблизим данный пример к реальности: будем просчитывать 100 следующих точек для шара. Алгоритм:
- создаем воркер, отправляем начальные данные, получаем массив координат для первых ста позиций;
- запускаем рендер (начинается отрисовка по уже имеющимся данным);
- пока она идет, в фоне запускаем вычисления следующих ста точек;
- когда достигли конца массива с полученными раннее данными, обновляем их свежими, продолжаем рисовать;
- в фоне запускаем вычисления следующей партии координат.
Основной код:
window.onload = function()
{
var canvas = document.getElementById("scene"), // канва
ctx = canvas.getContext("2d"),
canvasW = canvas.offsetWidth,
canvasH = canvas.offsetHeight,
posY,
posX = 0,
angle = 0,
r = 30,
counter = 0,
arrPosX = [],
arrPosY = [],
startAnim = true;
/* цвета и стили фигуры */
ctx.lineWidth = 2;
ctx.fillStyle = "green";
ctx.strokeStyle = "black";
/* если поддерживается web workers используем параллельные вычисления */
if(!!window.Worker)
{
var worker = new Worker("worker-2.js");
/* делаем запрос чтобы получить первые результаты */
worker.postMessage({
posX: posX,
angle: angle,
r: r,
canvasW: canvasW,
canvasH: canvasH
});
/* обработчик получения результат вычислений */
worker.onmessage = function (event) {
/*
данные сохряняем во временные переменные
чтобы их использовать когда нужно, а не когда завершатся вычисления
*/
var data = event.data,
arrPosXTemp = data.posX,
arrPosYTemp = data.posY,
angleTemp = data.angle;
// если это первый вызов обработчика, тогда запускаем рендер
if(startAnim==true)
{
requestAnimFrame(updateWorker);
startAnim = false;
arrPosX = arrPosXTemp; // первые полученные данные перегоняем в массивы, с которыми работает рендер
arrPosY = arrPosYTemp;
angle = angleTemp;
}
};
/* рендер */
function updateWorker()
{
ctx.clearRect(0, 0, 600, 400);
/* рисуем по вычисленным заранее координатам */
ctx.beginPath();
ctx.arc(arrPosX[counter], arrPosY[counter], r, 0, Math.PI*2, false);
ctx.closePath();
ctx.stroke();
ctx.fill();
/*
если счетчик = 0, запускаем в фоне вычисления новой партии координат
*/
if(counter===0)
{
worker.postMessage({
posX: arrPosX[99],
angle: angle,
r: r,
canvasW: canvasW,
canvasH: canvasH
});
counter++;
}
/*
если достигли конца просчитанных данных,
запрашиваем новую просчитанную партию из временных переменных
*/
else if(counter===99)
{
arrPosX = arrPosXTemp;
arrPosY = arrPosYTemp;
angle = angleTemp;
counter = 0;
}
else
{
counter++;
}
requestAnimFrame(updateWorker);
};
}
[...]
Код воркера:
onmessage = function(event) {
var data = event.data;
canvasH = data.canvasH,
angle = data.angle,
posX = data.posX,
canvasW = data.canvasW,
r = data.r,
arrPosX = [],
arrPosY = [];
/* вычиляем координаты 100 следующих точек */
arrPosX[0] = posX;
for(i=0; i<100; i++)
{
arrPosY[i] = canvasH/2 + Math.sin(angle) * 50;
angle += 0.05;
arrPosX[i+1]= arrPosX[i]+0.5;
if(arrPosX[i+1]>canvasW+r) arrPosX[i+1]=-r;
}
// отправляем назад результаты
postMessage({
posX: arrPosX,
posY: arrPosY,
angle: angle
});
}
Заметка
Воркеров можно запускать несколько.
Материалы
- MDN :: Using web workers
- Introduction to HTML5 Web Workers: Use Cases and Identify Hot Spots
- Web Workers за работой
