Эта статья является частью серии статей асинхронный и многопоточный алгоритм, и на данный момент мы построили продолжительный эволюционный алгоритм, который выполняет много математической работы, и мы сравнили последовательную версию с асинхронной версией. В результате, несмотря на то, что мы везде аккуратно возвращали промисы и вызывали каждую функцию с 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);
});

Приведенный выше вариант алгоритма, наконец, освобождает наш основной поток от какой-либо блокировки, оставляя его свободным для рендеринга результатов по мере необходимости.

Вот и все. Веб-воркеры смогли решить проблему, которую не смог решить асинхронный. Надеюсь, вам было интересно читать эту серию статей! Увидимся в следующем!