Статьи

HTML5 Web Workers: классический параллелизм передачи сообщений

Большинство сред параллелизма, о которых я пишу в этом блоге, состоят из множества уровней абстракции. Рассмотрим, например, Task Parallel Library: это оболочка поверх .NET Thread Pool, которая является оболочкой поверх потоков Windows. Это несоответствие низкоуровневых уровней абстракции вызывает определенные ожидания от более новых библиотек, а именно: они должны предоставлять прямой доступ к общему состоянию, обеспечивать механизмы синхронизации, переменные переменные, атомарные примитивы синхронизации,…

Похоже, что JavaScript (HTML5) с его стандартом Web Workers испытывает недостаток в абстракции для потоков в мире JavaScript. Поскольку не существует базовых низкоуровневых библиотек для многопоточных вычислений JavaScript (в браузере), веб-работники могут заново изобретать не только API, но и стиль параллелизма.

Веб-работники предоставляют модель передачи сообщений, в которой сценарии могут обмениваться данными только через четко определенные неизменяемые сообщения, не обмениваться данными и не использовать механизмы синхронизации для сигнализации или целостности данных. Действительно, веб-работники не сталкиваются с классическими проблемами параллелизма, такими как тупики и условия гонки — просто потому, что эти проблемы параллелизма невозможны.

Честно говоря, я немного завидую разработчикам JavaScript, которые теперь могут использовать многопоточность в своих приложениях на стороне браузера. API Web Workers минималистичен, но сделан правильно. Ниже приведен небольшой пример многопоточного поиска простых чисел с помощью Web Workers — если вы ищете более подробное введение и пошаговое руководство, ознакомьтесь со следующими ресурсами:

Во-первых, пользовательский интерфейс:

<input type="text" id="range_start"
       placeholder="start (e.g. 2)" /><br/>
<input type="text" id="range_end"
       placeholder="end (e.g. 100000)" /><br/>
<label for="dop">Degree of parallelism:</label>
<input type="range" id="dop"
       min="1" max="8" value="4" step="1" /><br/>
<input type="button" id="calculate" value="Calculate" />

образ

Теперь актуальный бизнес. При нажатии кнопки «Рассчитать» мы порождаем указанное количество рабочих потоков, чтобы выполнить расчет в фоновом режиме. Основной поток передает рабочим диапазон простых чисел, с которыми они будут работать, и получает от рабочих отчеты о ходе работы и итоговый счет:

//Some parts of the code elided for clarity
$(document).ready(function () {
    $("#calculate").click(function (e) {
        e.preventDefault();
        var rangeStart = parseInt($("#range_start").val());
        var rangeEnd = parseInt($("#range_end").val());
        var parallelism = parseInt($("#dop").val());
        createWorkers(parallelism, rangeStart, rangeEnd);
    });
});
function createWorkers(parallelism, start, end) {
    var range = end - start;
    var chunk = range / parallelism;
    var count = 0;
    var done = 0;
    for (var i = 0; i < parallelism; ++i) {
        var worker = new Worker("prime_finder.js");
        worker.onmessage = function(event) {
            if (event.data.type === 'DONE') {
                ++done;
                count += event.data.count;
                if (done == parallelism) ...
            } else if (event.data.type === 'PROGRESS') {
                var progress = event.data.value;
                ...
            }
        };
        var init = {
            start: start + i*chunk,
            end: start + (i+1)*chunk,
            idx: i
        };
        worker.postMessage(init);
    }
}

Обратите внимание на связь между потоками. Основной поток использует Worker.postMessage для предоставления данных рабочему потоку и получает от него обновления статуса с помощью события onmessage. Рабочий поток запускает скрипт prime_finder.js:

//The isPrime function elided for brevity
self.onmessage = function (event) {
    var start = event.data.start;
    var end = event.data.end;
    var size = end - start;
    var count = 0;
    for (var i = start; i < end; ++i) {
        if (isPrime(i)) ++count;
        if (i % 1000 === 0) {
            self.postMessage({
                type: 'PROGRESS',
                value: 100.0*((i-start)/size),
                idx: event.data.idx
            });
        }
    }
    self.postMessage({
        type: 'DONE',
        count: count,
        idx: event.data.idx
    });
};

Здесь мы видим противоположное направление — рабочий поток периодически публикует отчеты о проделанной работе и в конечном итоге сообщает о завершении. Все это запускается при получении исходного сообщения из основного потока.

образ
(На этом скриншоте вы можете видеть, как неравномерное распределение работы оказывается — первый поток заканчивается очень быстро, в то время как четвертый поток отстает довольно медленно …)

Чтобы поэкспериментировать с этим кодом, вы можете загрузить полную демонстрационную версию , включая очень маленький сервер Node.js, который обслуживает его.

Если у вас есть опыт разработки на C # или C ++, вам все это кажется неестественным. Где находится общее состояние? Где механизмы синхронизации? Где указатели на функции? — Действительно, может быть страшно написать многопоточную программу, использующую только асинхронную передачу сообщений — но это намного более чистый старт, чем многие библиотеки, которые мы имеем сегодня.

 

Источник: http://blogs.microsoft.co.il/blogs/sasha/archive/2012/02/05/html5-web-workers-classic-message-passing-concurrency.aspx