Знакомство с 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 следующих точек для шара. Алгоритм:

  1. создаем воркер, отправляем начальные данные, получаем массив координат для первых ста позиций;
  2. запускаем рендер (начинается отрисовка по уже имеющимся данным);
  3. пока она идет, в фоне запускаем вычисления следующих ста точек;
  4. когда достигли конца массива с полученными раннее данными, обновляем их свежими, продолжаем рисовать;
  5. в фоне запускаем вычисления следующей партии координат.

Основной код:

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
    });

	
}

В живую.

Заметка

Воркеров можно запускать несколько.

Материалы

Показать комментарии