Поиск по сайту:

Как обрабатывать задачи, связанные с ЦП, с помощью веб-воркеров


Автор выбрал программу Write for DOnations.

Введение

JavaScript обычно называют однопоточным языком, потому что код вашего веб-приложения выполняется последовательно один за другим в одном потоке. Если вы обращаетесь к веб-приложению на устройстве с несколькими ядрами, JavaScript использует только одно ядро. Когда задача выполняется в основном потоке, все последующие задачи должны ждать завершения задачи. Когда задача занимает много времени, она блокирует основной поток, предотвращая выполнение оставшихся задач. Большинство блокирующих задач, как правило, являются задачами с интенсивным использованием ЦП, также известными как задачи, связанные с ЦП, с такими примерами, как обработка графики, математические вычисления и сжатие видео или изображений.

В дополнение к задачам, связанным с процессором, у вас также будут задачи, связанные с вводом-выводом, которые не блокируются. Эти задачи, связанные с вводом-выводом, тратят большую часть времени на отправку запросов к операционной системе (ОС) и ожидание ответа. Примером может служить сетевой запрос промисов, определяющих функции, которые должны выполняться при разрешении промисов; то есть, когда операционная система завершает задачу и возвращает ответ.

Напротив, задачи, связанные с процессором, не простаивают, как задачи, связанные с вводом-выводом, которые ждут ОС. Задачи, связанные с ЦП, захватывают ЦП до завершения задачи, блокируя основной поток в процессе. Даже если вы завернете их в промис, они все равно будут блокировать основной поток. Кроме того, пользователи могут заметить, когда основной поток заблокирован, так как пользовательский интерфейс (UI) веб-приложения может зависнуть, а все, что использует JavaScript, может не работать.

В качестве решения этой проблемы браузеры представили Web Workers API для поддержки многопоточности в браузере. С помощью Web Workers вы можете разгрузить задачу, интенсивно использующую ЦП, в другой поток, что освобождает основной поток. Основной поток выполняет код JavaScript на одном ядре устройства, а разгруженная задача выполняется на другом ядре. Два потока могут взаимодействовать и обмениваться данными посредством передачи сообщений.

В этом руководстве вы создадите задачу с привязкой к процессору, которая блокирует основной поток в браузере, и посмотрите, как это влияет на веб-приложение. Затем вы безуспешно попытаетесь сделать задачу, привязанную к ЦП, неблокирующей, используя промисы. Наконец, вы создадите Web Worker, чтобы разгрузить задачу, привязанную к ЦП, в другой поток, чтобы она не блокировала основной поток.

Предпосылки

Чтобы следовать этому руководству, вам понадобятся:

  • Компьютер с двумя или более ядрами и установленным современным веб-браузером.
  • Локальная среда Node.js в вашей системе, которую можно настроить с помощью инструкции по установке Node.js и созданию локальной среды разработки.
  • Знания о цикле событий, обратных вызовах и промисах, которые вы можете узнать, прочитав статью Общие сведения о цикле событий, обратных вызовах, промисах и Async/Await в JavaScript.
  • Вам также потребуются базовые знания HTML, CSS и JavaScript, которые вы можете найти в нашем руководстве по программированию на JavaScript.

Шаг 1 — Создание задачи с привязкой к ЦП без веб-воркеров

На этом шаге вы создадите веб-приложение, в котором есть блокирующая задача, связанная с процессором, а также неблокирующие задачи. Приложение будет иметь три кнопки. Первая кнопка запустит задачу блокировки, представляющую собой цикл for, который повторяется около пяти миллиардов раз. Вторая кнопка будет увеличивать значение на веб-странице, а третья кнопка изменит цвет фона веб-приложения. Кнопки увеличения и изменения фона не блокируются.

Для начала создайте каталог проекта с помощью команды mkdir:

  1. mkdir workers_demo

Перейдите в каталог с помощью команды cd:

  1. cd workers_demo

Используя nano или ваш любимый текстовый редактор, создайте файл index.html:

  1. nano index.html

В файле index.html добавьте следующий код для создания кнопок и элементов div, отображающих выходные данные:

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Web Workers</title>
    <link rel="stylesheet" href="main.css" />
  </head>
  <body>
    <div class="wrapper">
      <div class="total-count"></div>
      <div class="buttons">
        <button class="btn btn-blocking" id="blockbtn">Blocking Task</button>
        <button class="btn btn-nonblocking" id="incrementbtn">Increment</button>
        <button class="btn btn-nonblocking" id="changebtn">
          Change Background
        </button>
      </div>
      <div class="output"></div>
    </div>
    <script src="main.js"></script>
  </body>
</html>

В разделе head вы ссылаетесь на таблицу стилей main.css, которая будет содержать стили приложения. В теге body вы создаете элемент div с классом total-count, который будет содержать значение, которое будет увеличиваться, когда кнопка нажата. Затем вы создаете еще один элемент div с тремя дочерними элементами button. Первая кнопка запустит задачу с интенсивным использованием ЦП, которая блокируется. Вторая кнопка будет увеличивать значение в элементе div с классом total-count, а третья кнопка запускает код JavaScript для изменения цвета фона. Эти две задачи являются неблокирующими.

Следующий элемент div будет содержать выходные данные задачи, интенсивно использующей ЦП, и, наконец, перед концом тега body вы ссылаетесь на main.js файл, который будет содержать весь код JavaScript.

Вы могли заметить, что элементы имеют идентификаторы и классы. Вы будете использовать их для ссылки на элементы в JavaScript позже на этом шаге.

Теперь сохраните и выйдите из файла.

Создайте и откройте файл main.css:

  1. nano main.css

В файле main.css добавьте следующее содержимое для стилизации элементов:

body {
  background: #fff;
  font-size: 16px;
}

.wrapper {
  max-width: 600px;
  margin: 0 auto;
}
.total-count {
  margin-bottom: 34px;
  font-size: 32px;
  text-align: center;
}

.buttons {
  border: 1px solid green;
  padding: 1rem;
  margin-bottom: 16px;
}

.btn {
  border: 0;
  padding: 1rem;
}

.btn-blocking {
  background-color: #f44336;
  color: #fff;
}

#changebtn {
  background-color: #4caf50;
  color: #fff;
}

.buttons определяются со сплошной зеленой рамкой и светлым отступом, но задача блокировки дополнительно определяется стилем .btn-blocking, в котором используется другой цвет фона.

Сохраните и закройте файл.

Теперь, когда вы определили стили CSS, вы напишете код JavaScript, чтобы сделать HTML-элементы интерактивными. Сохраните и выйдите из файла.

Создайте и откройте файл main.js в своем редакторе:

  1. nano main.js

В файле main.js добавьте следующий код для ссылки на элементы DOM:

const blockingBtn = document.getElementById("blockbtn");
const incrementBtn = document.getElementById("incrementbtn");
const changeColorBtn = document.getElementById("changebtn");
const output = document.querySelector(".output");
const totalCountEl = document.querySelector(".total-count");

В первых трех строках вы ссылаетесь на кнопки с их идентификаторами, используя метод getElementByID() объекта document. В последних двух строках вы ссылаетесь на элементы div с именами их классов, используя метод querySelector() объекта document.

Затем определите прослушиватель событий, который будет увеличивать значение элемента div при нажатии кнопки incrementBtn:

...
totalCountEl.textContent = 0;

incrementBtn.addEventListener("click", function incrementValue() {
  let counter = totalCountEl.textContent;
  counter++;
  totalCountEl.textContent = counter;
});

Сначала вы устанавливаете для текстового содержимого элемента totalCountEl значение 0. Затем вы прикрепляете прослушиватель событий к кнопке incrementBtn с помощью метода DOM addEventListener(). Метод принимает два аргумента: событие для прослушивания и обратный вызов. Здесь прослушиватель событий прослушивает событие click и вызывает обратный вызов incrementValue(), когда событие click было запущено.

В обратном вызове incrementValue() вы извлекаете значение текстового содержимого totalCountEl из модели DOM и устанавливаете его в переменную counter. Затем вы увеличиваете значение на 1 и устанавливаете для текстового содержимого элемента totalCountEl увеличенное значение.

Затем добавьте следующий код, чтобы привязать событие клика к кнопке changeColorBtn, чтобы цвет фона случайным образом менялся при нажатии кнопки:

...
changeColorBtn.addEventListener("click", function changeBackgroundColor() {
  colors = ["#009688", "#ffc107", "#dadada"];
  const randomIndex = Math.floor(Math.random() * colors.length)
  const randomColor = colors[randomIndex];
  document.body.style.background = randomColor;
});

В предыдущем коде вы прикрепляете прослушиватель событий щелчка, который запускает обратный вызов changeBackgroundColor, когда пользователь нажимает кнопку changeColorBtn. В обратном вызове вы устанавливаете переменную colors в массив из трех значений цвета HEX. Затем вы вызываете метод Math.random() и умножаете его результат на значение длины массива, чтобы сгенерировать случайное число между 0 и длиной массива 3. Затем случайное значение округляется до ближайшего целого числа с помощью метода Math.Floor() и сохраняется в переменной randomIndex.

После этого вы выбираете значение из массива, используя случайный индекс, а затем устанавливаете этот цвет для свойства body.style.background объекта document.

Теперь, когда вы реализовали две кнопки, запускающие выполнение неблокирующих задач, вы прикрепите прослушиватель событий к оставшейся кнопке, чтобы запустить задачу, интенсивно использующую ЦП. Цикл повторится пять миллиардов раз и сохранит результат в DOM.

По-прежнему в файле main.js добавьте следующий код, чтобы прикрепить прослушиватель событий клика к кнопке, которая запускает задачу блокировки:

...
blockingBtn.addEventListener("click", function blockMainThread() {
  let counter = 0;
  for (let i = 0; i < 5_000_000_000; i++) {
    counter++;
  }
  output.textContent = `Result: ${counter}`;
});

В приведенном выше коде вы подключаете прослушиватель событий щелчка, который запускает обратный вызов blockMainThread(). Внутри функции вы устанавливаете для counter значение 0, а затем создаете цикл, который повторяется пять миллиардов раз. Во время каждой итерации значение counter увеличивается на 1. После завершения цикла результат вычисления устанавливается в элемент output.

Полный файл теперь будет соответствовать следующему:

const blockingBtn = document.getElementById("blockbtn");
const incrementBtn = document.getElementById("incrementbtn");
const changeColorBtn = document.getElementById("changebtn");
const output = document.querySelector(".output");
const totalCountEl = document.querySelector(".total-count");
totalCountEl.textContent = 0;

incrementBtn.addEventListener("click", function incrementValue() {
  let counter = totalCountEl.textContent;
  counter++;
  totalCountEl.textContent = counter;
});

changeColorBtn.addEventListener("click", function changeBackgroundColor() {
  colors = ["#009688", "#ffc107", "#dadada"];
  const randomIndex = Math.floor(Math.random() * colors.length)
  const randomColor = colors[randomIndex];
  document.body.style.background = randomColor;
});

blockingBtn.addEventListener("click", function blockMainThread() {
  let counter = 0;
  for (let i = 0; i < 5_000_000_000; i++) {
    counter++;
  }
  output.textContent = `Result: ${counter}`;
});

Закончив ввод кода, сохраните и закройте файл.

Чтобы избежать ошибок Cross-Origin Resource Sharing (CORS) при начале работы с веб-воркерами на шаге 3, вам необходимо создать веб-сервер для приложения. Выполните следующую команду, чтобы создать сервер:

  1. npx serve .

Введите y для подтверждения, и консоль выведет сообщение Serving!, подтверждающее, что сервер запущен:

Output
┌─────────────────────────────────────────────────────┐ │ │ │ Serving! │ │ │ │ - Local: http://localhost:3000 │ │ - On Your Network: http://your_ip_address:3000 │ │ │ │ Copied local address to clipboard! │ │ │ └─────────────────────────────────────────────────────┘

Откройте предпочитаемый веб-браузер и посетите http://localhost:3000/index.html.

Примечание. Если вы выполняете руководство на удаленном сервере, вы можете просмотреть файл index.html в своем браузере, используя переадресацию портов.

В текущем терминале запустите веб-сервер с помощью следующей команды:

  1. npx serve .

При появлении запроса введите y, чтобы продолжить.

Ваша консоль может загрузить следующую ошибку, но это не должно повлиять на вашу возможность доступа к веб-серверу:

Output
ERROR: Cannot copy server address to clipboard: Couldn't find the `xsel` binary and fallback didn't work. On Debian/Ubuntu you can install xsel with : sudo apt install xsel. ┌─────────────────────────────────────────────────────┐ │ │ │ Serving! │ │ │ │ - Local: http://localhost:3000 │ │ - On Your Network: http://your_ip_address:3000 │ │ │ │ Copied local address to clipboard! │ │ │ └─────────────────────────────────────────────────────┘

Откройте второй терминал на локальном компьютере и введите следующую команду:

  1. ssh -L 3000:localhost:3000 your_non_root_user@your_server_ip

Вернитесь в браузер и перейдите по адресу http://localhost:3000/index.html, чтобы получить доступ к домашней странице вашего приложения.

Когда страница загрузится, на ней отобразится домашняя страница с кнопками «Блокировка задачи», «Увеличение» и «Изменить фон». Счетчик приращения начнется с 0, потому что вы еще не нажали кнопку для увеличения счетчика:

Сначала несколько раз нажмите кнопку «Увеличить», чтобы число на странице обновлялось при каждом нажатии:

Во-вторых, нажмите кнопку «Изменить фон» несколько раз, чтобы изменить цвет фона страницы:

Наконец, нажмите кнопку «Блокировка задачи», затем случайным образом нажмите кнопки «Увеличить» и «Изменить фон». Страница перестанет отвечать, и кнопки не будут работать. Это зависание происходит из-за того, что кнопка «Блокирующая задача» запускает задачу с интенсивным использованием ЦП, которая заблокировала основной поток, и никакой другой код не будет выполняться, пока основной поток не освободится. По прошествии некоторого времени и завершении задачи, интенсивно использующей ЦП, на странице будет отображаться Результат: 5000000000. В этот момент, если вы нажмете другие кнопки, они снова начнут работать.

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

Теперь, когда вы создали приложение с блокирующей задачей, которая замораживает приложение через основной поток, вы будете использовать промисы для преобразования задач, связанных с ЦП, в неблокирующие задачи.

Шаг 2 — Разгрузка задачи, привязанной к процессору, с помощью промисов

Использование Fetch API или других методов на основе промисов для обработки задач ввода-вывода иногда создает неправильное впечатление, что обертывание задачи, привязанной к процессору, в промис может сделать ее неблокирующей. Как упоминалось во введении, задачи ввода-вывода не блокируются, потому что они обрабатываются операционной системой, которая уведомляет механизм JavaScript о завершении задачи. Пока операционная система выполняет задачу ввода-вывода, обратные вызовы, связанные с задачами ввода-вывода, ожидают ответа от ОС в очереди. Пока они ждут в очереди, основной поток свободен для обработки всех последующих задач. Когда от ОС приходит ответ, обратные вызовы выполняются в основном потоке, и параллельного выполнения обратных вызовов нет.

Чтобы продемонстрировать, что промисы не делают задачу, связанную с ЦП, неблокирующей, на этом шаге вы заключите задачу, интенсивно использующую ЦП, в промис.

В текстовом редакторе откройте файл main.js:

  1. nano main.js

В файле main.js добавьте выделенный код, чтобы создать функцию calculateCount(), которая заключает задачу, интенсивно использующую ЦП, в промис:

...
function calculateCount() {
  return new Promise((resolve, reject) => {
    let counter = 0;
    for (let i = 0; i < 5_000_000_000; i++) {
      counter++;
    }
    resolve(counter);
  });
}

blockingBtn.addEventListener("click", function blockMainThread(){
  ....
})

Функция calculateCount() возвращает обещание. В этой функции вы инициализируете промис, используя синтаксис new Promise, который принимает обратный вызов, который принимает параметры resolve и reject. Параметры обрабатывают успех или неудачу операции в обратном вызове. Обратный вызов содержит цикл с интенсивным использованием ЦП, который повторяется пять миллиардов раз. После завершения цикла вы вызываете метод resolve с результатом.

Теперь, когда у вас есть задача, привязанная к процессору, в функции calculateCount(), удалите выделенный код:

...
blockingBtn.addEventListener("click", function blockMainThread() {
  let counter = 0;
  for (let i = 0; i < 5_000_000_000; i++) {
    counter++;
  }
  output.textContent = `Result: ${counter}`;
});

После удаления кода вы вызовете функцию calculateCount() в функции blockMainThread(). Поскольку функция возвращает обещание, вам нужен синтаксис async/await, чтобы использовать обещание.

Добавьте выделенный код, чтобы сделать функцию blockMainThread() асинхронной, и вызовите функцию calculateCount():

...
blockingBtn.addEventListener("click", async function blockMainThread() {
  const counter = await calculateCount();
  output.textContent = `Result: ${counter}`;
});

В предыдущем коде вы добавляете префикс функции blockMainThread() с ключевым словом async, чтобы сделать ее асинхронной. Внутри функции вы ставите перед функцией calculateCount() ключевое слово await и вызываете функцию. Оператор await ожидает разрешения промиса. После разрешения переменной counter присваивается возвращаемое значение, а элементу div output присваивается результат задачи, связанной с процессором.

Ваш полный файл теперь будет соответствовать следующему:

const blockingBtn = document.getElementById("blockbtn");
const incrementBtn = document.getElementById("incrementbtn");
const changeColorBtn = document.getElementById("changebtn");
const output = document.querySelector(".output");
const totalCountEl = document.querySelector(".total-count");
totalCountEl.textContent = 0;

incrementBtn.addEventListener("click", function incrementValue() {
  let counter = totalCountEl.textContent;
  counter++;
  totalCountEl.textContent = counter;
});

changeColorBtn.addEventListener("click", function changeBackgroundColor() {
  colors = ["#009688", "#ffc107", "#dadada"];
  const randomIndex = Math.floor(Math.random() * colors.length)
  const randomColor = colors[randomIndex];
  document.body.style.background = randomColor;
});

function calculateCount() {
  return new Promise((resolve, reject) => {
    let counter = 0;
    for (let i = 0; i < 5_000_000_000; i++) {
      counter++;
    }
    resolve(counter);
  });
}

blockingBtn.addEventListener("click", async function blockMainThread() {
  const counter = await calculateCount();
  output.textContent = `Result: ${counter}`;
});

После внесения изменений сохраните и закройте файл.

Пока ваш сервер все еще работает, обновите http://localhost:3000/index.html в своем браузере. Нажмите кнопки «Увеличить» и «Изменить фон». После этого нажмите кнопку Блокировка задачи, а затем нажмите другие кнопки. Другие кнопки по-прежнему не реагируют, когда выполняется задача с привязкой к ЦП, что доказывает, что обертывание задачи с привязкой к ЦП в обещание не делает задачу неблокирующей.

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

Шаг 3 — Разгрузка задачи, связанной с процессором, с помощью веб-воркеров

На этом шаге вы создадите выделенного рабочего процесса для разгрузки задачи, связанной с ЦП, путем перемещения задачи, привязанной к ЦП, в файл worker.js. В файле main.js вы создадите экземпляр выделенного Web Worker с путем к файлу worker.js. После инициализации Web Worker задача, связанная с ЦП, будет выгружена в отдельный поток, а основной поток будет свободен для обработки оставшихся задач.

Сначала создайте файл worker.js:

  1. nano worker.js

В файле worker.js добавьте следующий код, чтобы добавить в файл задачу, связанную с процессором:

let counter = 0;
for (let i = 0; i < 5_000_000_000; i++) {
  counter++;
}

Предыдущий блок кода содержит задачу, привязанную к ЦП, которую вы использовали до сих пор. Теперь этот код будет выполняться в отдельном потоке.

Чтобы основной поток мог получить доступ к результату вычисления, вам необходимо отправить сообщение, содержащее данные, с помощью метода postMessage() интерфейса Worker.

В файле worker.js добавьте выделенную строку для отправки данных в основной поток:

let counter = 0;
for (let i = 0; i < 5_000_000_000; i++) {
  counter++;
}
postMessage(counter);

В этой строке вы вызываете метод postMessage() с переменной counter, которая содержит результат расчета задачи, связанной с процессором.

Сохраните и закройте файл.

Теперь, когда вы переместили задачу, связанную с процессором, в worker.js, откройте файл main.js:

  1. nano main.js

Удалите выделенные строки, содержащие задачу, связанную с процессором, в файле main.js:

...
function calculateCount() {
  return new Promise((resolve, reject) => {
    let counter = 0;
    for (let i = 0; i < 5_000_000_000; i++) {
      counter++;
    }
    resolve(counter);
  });
}

blockingBtn.addEventListener("click", async function blockMainThread() {
  const counter = await calculateCount();
  output.textContent = `Result: ${counter}`;
});

В обратном вызове blockMainThread добавьте выделенный код для инициализации рабочего потока и прослушивания сообщений из рабочего потока:

blockingBtn.addEventListener("click", function blockMainThread() {
  const worker = new Worker("worker.js");
  worker.onmessage = (msg) => {
    output.textContent = `Result: ${msg.data}`;
  };
});

Сначала вы создаете экземпляр Worker с путем к файлу worker.js, который вы создали ранее. Во-вторых, вы присоединяете свойство onmessage интерфейса Worker к потоку worker, который будет прослушивать любые сообщения, приходящие из рабочего потока. Если есть входящее сообщение, запускается событие message, которое вызывает обратный вызов с данными сообщения msg в качестве аргумента. В обратном вызове вы изменяете текстовое содержимое output с помощью сообщения, полученного от Web Worker.

Теперь полный файл будет соответствовать следующему блоку кода:

const blockingBtn = document.getElementById("blockbtn");
const incrementBtn = document.getElementById("incrementbtn");
const changeColorBtn = document.getElementById("changebtn");
const output = document.querySelector(".output");
const totalCountEl = document.querySelector(".total-count");
totalCountEl.textContent = 0;

incrementBtn.addEventListener("click", function incrementValue() {
  let counter = totalCountEl.textContent;
  counter++;
  totalCountEl.textContent = counter;
});

changeColorBtn.addEventListener("click", function changeBackgroundColor() {
  colors = ["#009688", "#ffc107", "#dadada"];
  const randomIndex = Math.floor(Math.random() * colors.length)
  const randomColor = colors[randomIndex];
  document.body.style.background = randomColor;
});

blockingBtn.addEventListener("click", function blockMainThread() {
  const worker = new Worker("worker.js");
  worker.onmessage = (msg) => {
    output.textContent = `Result: ${msg.data}`;
  };
});

Сохраните и закройте файл.

Когда сервер запущен, вернитесь в веб-браузер и посетите http://localhost:3000/index.html. Страница будет успешно загружена с сервера.

Сначала несколько раз нажмите кнопки «Увеличить» и «Изменить фон». Во-вторых, нажмите кнопку Блокирующая задача, чтобы запустить задачу, интенсивно использующую ЦП, а затем продолжайте нажимать другие кнопки. Кнопки теперь будут работать без проблем, несмотря на то, что задача с интенсивным использованием ЦП все еще выполняется.

Теперь вы можете разгрузить задачу, интенсивно использующую ЦП, с помощью выделенного веб-воркера, чтобы сделать ее неблокирующей.

Заключение

В этом руководстве вы создали приложение, которое запускает задачу, связанную с ЦП, которая блокирует основной поток. Затем вы безуспешно пытались использовать обещания, чтобы сделать задачу, связанную с процессором, неблокирующей. Наконец, вы использовали выделенный Web Worker, чтобы разгрузить задачу, привязанную к ЦП, в другой поток, чтобы сделать ее неблокирующей.

В качестве следующего шага вы можете посетить Service Workers, которые можно использовать для предоставления автономного доступа и повышения производительности.

Если вы используете Node.js, вы можете узнать, как использовать рабочие потоки, в разделе Как использовать многопоточность в Node.js.