Multithreading in JavaScript

Alex Saskevich, @asaskevich

for (var i = 0; i < 10e10; i ++) {
    doSomeAwesomeWork();
}

Страницу можно "подвесить" вот так:

Или вот так:

var doSomeWork = function() {
  doSomeHardCalculations();
}
function someThread(id) {
    setInterval(function() { 
        console.log('Hi from thread #id =', id); 
    }, 20);
}

someThread(1);
someThread(2);
someThread(3);
someThread(4);
someThread(5);

Один из вариантов реализации "потоков"

Использование setTimeout для реализации асинхронных вычислений.

var doCalculations = function(n) {
    setTimeout(function() {
        var i = 0;
        var res = 0;
        while (i < n) {
            res += i;
            i++;
        }
    }, 0);
}

Web Workers помогут в реализации многопоточности!

Доступность Web Workers в современных браузерах:

var worker = new Worker('worker.js');
//..
var worker = new Worker('scripts/worker.js');

Код воркера должен быть расположен в отдельном файле.

worker.postMessage('hi'); 
worker.addEventListener('message', function(e) {
  console.log('Worker said: ', e.data);
}, false);

Взаимодействие потоков между собой:

Примерная схема взаимодействия потоков:

// Передавать можно либо строки,
worker.postMessage('Hello World');
// либо JSON объекты
worker.postMessage({msg: 'say hello'});

Что можно передать в другой поток:

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

self для веб-воркера имеет тот же смысл, что и this для основного скрипта

// Основной скрипт
worker.terminate();

...

// worker.js
self.close();

Как остановить созданный поток?

  • Объект navigator

  • Объект location (read-only)

  • XMLHttpRequest

  • setTimeout() / clearTimeout()

  • setInterval() / clearInterval()

  • Кэш приложений

  • Импорт внешних скриптов с использованием метода importScripts()

  • Создание новых воркеров

Web Workers имеют доступ к:

Web Workers не могут работать с:

  • Моделью DOM

  • document

  • window

  • parent

// worker.js

// Ничего не загрузит
importScripts(); 
// Загрузит script.js
importScripts('script.js'); 
// Загрузит foo.js и bar.js
importScripts('foo.js', 'bar.js'); 

Импорт внешних скриптов

// Создаем на основе BlobBuilder 
// экземпляр встроенного воркера
var code = 'onmessage = function(e) { ... }';
var blob = new BlobBuilder();
blob.append(code);
blob = blob.getBlob();

Встроенный воркер при помощи BlobBuilder

// Создаем на основе Blob 
// экземпляр встроенного воркера
var code = 'onmessage = function(e) { ... }';
var blob = new Blob([code], 
              {type: 'application/javascript'});

Встроенный воркер при помощи Blob *

Chrome 8+, Firefox 6+, Safari 6.0+, Opera 15+

var blobURL = URL.createObjectURL(blob);
var worker = new Worker(blobURL);

Создание воркера на основе Blob

// blobURL имеет примерно такой вид 
// a923da41-3ce9-4d1a-f145-eacs532539b1
URL.revokeObjectURL(blobURL);

Удалить ненужный Blob можно так:

<script id="worker" type="javascript/worker">
  // Код воркера
  // ...
</script>
// ...
var code = document.querySelector('#worker').textContent;
var blob = new Blob([code], 
              {type: 'application/javascript'});

Конечный пример создания встроенного воркера

function errorHandler(e) {
    // e.lineno - номер строки
    // e.filename - имя файла
    // e.message - сообщение об ошибке
    // ...
}

worker.addEventListener('error', errorHandler, false);

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

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

  • Скрипты и страница вызова должны относиться к одному источнику

  • Объекты Worker не запускаются локально

    • * только для Chrome

    • --allow-file-access-from-files

Shared Workers

var worker = new SharedWorker("script.js");

Создание объекта SharedWorker

worker.port.addEventListener('message', 
            function(e) { .. }, false);

Обработка событий:

worker.port.start();

Запуск Shared Worker

worker.port.postMessage('hi');

Посылка сообщений внутрь потока

var ports = [];

onconnect = function(event) {
    var port = event.ports[0];
    ports.push(port);
    port.start();
    port.addEventListener("message",
        function(e) { handler(e, port); } );
}

handler = function(e, port) {
    port.postMessage('hello');
}

Примерная реализация SharedWorkera

Применение Web Workerов

  • Кеширование данных

  • Проверка правописания

  • Обработка аудио/видео

  • Фоновые операции ввода/вывода

  • Работа с веб-службами

  • Обработка больших объемов данных

  • И так далее...

Имитация Web Workers в устаревших браузерах

fakeworker-js

Имитирует многопоточность при помощи setTimeout() & eval()

Удобен для отладки веб-воркеров

Simulated Web Workers

Создает невидимый iframe, внутри которого и работает скрипт воркера

ie-web-worker

Симулирует Web Workers в IE

Аналоги, реализующие многопоточность

Multithread.js

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

<script src="path/to/multithread.js"></script>
var threadCount = 4;
var MT = new Multithread(threadCount);
MT.process(function(a, b, c) {
    // некоторые вычисления
    return result;
}, function callback(result) {
    // обработка результата
})(a, b, c);

Асинхронный запуск функции

function scopeCheck() {
  var scopeVar = 2;
  MT.process(
    // scopeVar is not defined
    function() { return scopeVar + 2; },
    function(r) { console.log('Hop-hey'); }
  )();
}
scopeCheck();

Область видимости потока

Ограничения

  • Все данные, передаваемые в поток, должны быть JSON сериализуемы

  • Потоки не должны работать с DOM

Parallel.js

Установка

<script src="parallel.js"></script>
$ npm install paralleljs

Инициализация

var p = new Parallel(someData);

Запуск потока

var slowSquare = function (n) { 
    var i = 0; 
    while (++i < n * n) {}
    return i; 
};

var p = new Parallel(100000);
p.spawn(slowSquare).then(function() { 
    alert('Completed!'); 
});

map - последовательная обработка входных данных

var p = new Parallel([0, 1, 2, 3, 4, 5, 6]),
    log = function () { console.log(arguments); };

function fib(n) {
  return n < 2 ? 1 : fib(n - 1) + fib(n - 2);
};
 
p.map(fib).then(log)

reduce - свертка обработанных данных 

var p = new Parallel(_.range(50));
 
function add(d) { return d[0] + d[1]; }
function log() { console.log(arguments); }

p.map(function (n) { 
    return Math.pow(10, n); 
}).reduce(add).then(log);

Рекомендуется к прочтению

Web Workers

by Ido Green

The End.