Большинство сред параллелизма, о которых я пишу в этом блоге, состоят из множества уровней абстракции. Рассмотрим, например, Task Parallel Library: это оболочка поверх .NET Thread Pool, которая является оболочкой поверх потоков Windows. Это несоответствие низкоуровневых уровней абстракции вызывает определенные ожидания от более новых библиотек, а именно: они должны предоставлять прямой доступ к общему состоянию, обеспечивать механизмы синхронизации, переменные переменные, атомарные примитивы синхронизации,…
Похоже, что JavaScript (HTML5) с его стандартом Web Workers испытывает недостаток в абстракции для потоков в мире JavaScript. Поскольку не существует базовых низкоуровневых библиотек для многопоточных вычислений JavaScript (в браузере), веб-работники могут заново изобретать не только API, но и стиль параллелизма.
Веб-работники предоставляют модель передачи сообщений, в которой сценарии могут обмениваться данными только через четко определенные неизменяемые сообщения, не обмениваться данными и не использовать механизмы синхронизации для сигнализации или целостности данных. Действительно, веб-работники не сталкиваются с классическими проблемами параллелизма, такими как тупики и условия гонки — просто потому, что эти проблемы параллелизма невозможны.
Честно говоря, я немного завидую разработчикам JavaScript, которые теперь могут использовать многопоточность в своих приложениях на стороне браузера. API Web Workers минималистичен, но сделан правильно. Ниже приведен небольшой пример многопоточного поиска простых чисел с помощью Web Workers — если вы ищете более подробное введение и пошаговое руководство, ознакомьтесь со следующими ресурсами:
- Использование веб-работников — Mozilla Developer Network
- Веб-работники в HTML5 — Гил Финк
- Работа с Inline Web Workers — Гил Финк
- WebWorkers — HTML5 — Шломо Голдберг [HEB]
- Web Workers — W3C Draft
Во-первых, пользовательский интерфейс:
<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 ++, вам все это кажется неестественным. Где находится общее состояние? Где механизмы синхронизации? Где указатели на функции? — Действительно, может быть страшно написать многопоточную программу, использующую только асинхронную передачу сообщений — но это намного более чистый старт, чем многие библиотеки, которые мы имеем сегодня.