Эта статья является частью серии статей асинхронный и многопоточный алгоритм, и на данный момент мы построили продолжительный эволюционный алгоритм, который выполняет много математической работы, и мы сравнили последовательную версию с асинхронной версией. В результате, несмотря на то, что мы везде аккуратно возвращали промисы и вызывали каждую функцию с await
, веб-интерфейс всегда был полностью заблокирован и заморожен, потому что вся работа выполнялась в одном потоке.
Решение, конечно, в этом случае не async/await. Как мы видели практически в прошлый раз, для работы своего волшебного async требуется другая система для разгрузки вызова: диск, база данных, сеть, другой процесс, что угодно. Если мы вызовем await
для функции, которая выполняет свою работу в том же основном потоке, из которого мы вызываем, мы ничего не получим. Значит ли это, что у нас не может быть алгоритма искусственного интеллекта, полностью работающего в javascript, или какой-либо математической длительной задачи, которую мы не можем перенести в другой API или систему? Да мы можем. Давайте поговорим о веб-воркерах!
Краткое введение в веб-воркеры
Веб-воркер — это файл javascript, который выполняется в отдельном потоке. Каждый веб-воркер, который мы загружаем на HTML-страницу, работает в своем собственном потоке. Мы можем загрузить веб-воркеры, используя следующий синтаксис javascript:
const worker = new Worker("worker.js", { type: "module" });
Если мы используем type: “module”
, файл worker.js
будет рассматриваться как модуль javascript, то есть он сможет использовать операторы import
. Помимо этого, worker.js
— это обычный файл сценария, как и любой другой, в который мы можем добавить код, необходимый для запуска в другом потоке. Чтобы основной поток и рабочий поток могли взаимодействовать, мы используем события:
worker.postMessage('hello'); worker.addEventListener("message", (e) => { console.log(e.data); });
В приведенном выше фрагменте кода мы отправляем сообщение hello
рабочему процессу и настраиваем обработчик события message
для перехвата любых сообщений, поступающих от рабочего процесса. Точно так же worker может отправлять и получать сообщения от любых слушателей в основном потоке:
addEventListener("message", (e) => { console.log(e.data); }); const result = 2 + 2; postMessage(result);
В приведенном выше фрагменте мы вычисляем 2 + 2
в файле worker.js
и отправляем результат в основной поток, используя postMessage
. Обратите внимание, что мы можем использовать другие веб-воркеры в веб-воркере, инициализируя их с помощью синтаксиса new Worker
.
Переписываем наш эволюционный алгоритм для использования веб-воркеров
Это все, что нам нужно для успешного запуска нашего эволюционного алгоритма в веб-воркере. Ожидается, что веб-интерфейс будет обновляться с каждым полученным результатом, каждый раз, когда появляется новое поколение популяции. Код доступен на Github. Прежде всего, давайте создадим наш рабочий файл, worker.js
:
import { createRandomMatrix } from "./random.js"; import { evolvePopulation } from "./evolution.js"; import { fitness, pickBest } from "./operations.js"; import { same } from "./tools.js"; let population = createRandomMatrix(5000, 20); let bestFitnessAchieved = false; let previousBest = null; while (!bestFitnessAchieved) { population = evolvePopulation(population, 0.4); const best = pickBest(...population); const bestFitness = fitness(best); if (bestFitness.toFixed(2) == 0.0) bestFitnessAchieved = true; if (!same(previousBest, best)) { previousBest = best; postMessage(best); } }
По сути, мы перемещаем весь механизм эволюции в воркера. Мы хотим достичь наилучшей доступной физической подготовки, которая равна нулю, поэтому, пока мы этого не делаем, мы продолжаем развивать новые поколения. Каждый раз, когда у нас появляется новое поколение, мы публикуем сообщение в вызывающей ветке с лучшим человеком. Обратите внимание, как мы импортируем все необходимые нам javascript-модули: это сделает worker модулем и потребует параметр type: “module”
при его инициализации. Тем временем наш index.js
теряет всю эволюционную логику, получает результат каждый раз, когда поток его производит, и отображает его на странице:
import { roundAll } from "./tools.js"; import { fitness } from "./operations.js"; const worker = new Worker("worker.js", { type: "module" }); worker.addEventListener("message", (e) => { const result = e.data; const rounded = roundAll(result); const item = document.createElement("p"); const values = document.createElement("span"); const meter = document.createElement("meter"); meter.min = 0; meter.max = 50; meter.value = (50 - fitness(result)).toFixed(2); meter.style.width = "100%"; meter.style.height = "100%"; values.innerText = `${rounded.join(" ")}: ${(50 - fitness(result)).toFixed( 2 )}`; item.style.display = "grid"; item.style.gridTemplateColumns = "5em 1fr"; item.style.columnGap = "1em"; item.appendChild(meter); item.appendChild(values); document.querySelector("main").appendChild(item); });
Приведенный выше вариант алгоритма, наконец, освобождает наш основной поток от какой-либо блокировки, оставляя его свободным для рендеринга результатов по мере необходимости.
Вот и все. Веб-воркеры смогли решить проблему, которую не смог решить асинхронный. Надеюсь, вам было интересно читать эту серию статей! Увидимся в следующем!