
JavaScript — это универсальный язык программирования, известный своей высокоуровневой природой, однопоточной моделью выполнения, сборкой мусора, динамической типизацией и объектной ориентацией на основе прототипов.
В этом сообщении блога мы рассмотрим концепцию однопоточной природы JavaScript и углубимся в то, как мы можем реализовать возможности многопоточности с помощью таких инструментов, как Node.js Worker Threads и browser Web Workers.
Используя эти технологии, разработчики могут эффективно использовать несколько потоков выполнения в JavaScript для повышения производительности и скорости реагирования.
Как JavaScript работает сейчас?
JavaScript работает как однопоточный язык, что означает, что он в основном использует одно ядро ЦП, часто называемое «основным потоком». Несмотря на это очевидное ограничение, JavaScript способен выполнять код одновременно благодаря своему неблокирующему циклу событий. механизм.
Например, рассмотрим трудоемкую операцию, такую как получение данных из внешнего API:
fetch('some-url').then(doSomethingLater);
В этом сценарии вызов fetch выполняется вне основного потока. После завершения получения данных соответствующая функция обратного вызова (doSomethingLater) ставится в очередь для выполнения в основном потоке. Такой подход гарантирует, что, хотя основной поток не блокируется во время процесса выборки, он все равно может эффективно выполнять другие задачи.
Рабочие потоки Nodejs
// Creating an array of 100 elements, each with a value of 1 billion
const jobs = Array.from(
{
length: 100,
},
() => 1e9
);
// Recording the starting time
const tick = performance.now();
// Recording the starting time
for (let job of jobs) {
let count = 0;
// Inner loop to increment a counter from 0 to the current element's value
for (let i = 0; i < job; i++) {
count++;
}
}
// Recording the ending time
const tock = performance.now();
// Calculating and displaying the time taken for the main thread to complete the task
console.log(`Main thread took ${tock - tick}ms`);
Этот фрагмент кода демонстрирует использование рабочих потоков Node.js для измерения производительности движка JavaScript при выполнении цикла со значительным количеством итераций. Массив jobs содержит 100 элементов, каждый из которых инициализирован значением 1 миллиард. Цель состоит в том, чтобы перебрать каждый элемент массива и выполнить внутренний цикл, который увеличивает счетчик от 0 до значения текущего элемента. Время, необходимое для выполнения этой задачи, измеряется с помощью функции performance.now().
После запуска программы выходные данные показывают время, необходимое основному потоку для выполнения цикла. Имейте в виду, что фактическая производительность может варьироваться в зависимости от характеристик процессора вашего компьютера. В этом примере время выполнения примерно следующее на компьютере с процессором Intel(R) Core(TM) i7–1165G7 11-го поколения с частотой 2,80 ГГц.
Main thread took 81244.84360000491ms
Использование рабочих потоков Node.js для параллелизма:
// index.js
const { Worker } = require("worker_threads");
// Function to divide an array into chunks
function chunkify(array, n) {
let chunks = [];
for (let i = n; i > 0; i--) {
chunks.push(array.splice(0, Math.ceil(array.length / i)));
}
return chunks;
}
// Function to run tasks concurrently using worker threads
function run(jobs, concurrentWorkers) {
const chunks = chunkify(jobs, concurrentWorkers);
const tick = performance.now();
let completedWorkers = 0;
chunks.forEach((data, i) => {
const worker = new Worker("./worker.js");
worker.postMessage(data);
worker.on("message", () => {
console.log(`Worker ${i} completed`);
completedWorkers++;
if (completedWorkers === concurrentWorkers) {
const tock = performance.now();
console.log(`${concurrentWorkers} workers took ${tock - tick}ms`);
process.exit();
}
});
});
}
// Generating an array of 100 elements, each with a value of 1 billion
const jobs = Array.from(
{
length: 100,
},
() => 1e9
);
// Running tasks with different numbers of concurrent workers
run(jobs, 2);
const { parentPort } = require("worker_threads");
parentPort.on("message", (jobs) => {
for (let job of jobs) {
let count = 0;
for (let i = 0; i < job; i++) {
count++;
}
}
parentPort.postMessage("done");
});
Этот код иллюстрирует использование рабочих потоков Node.js для одновременного выполнения задач. Сценарий начинается с определения функции chunkify, которая делит массив на более мелкие фрагменты в зависимости от желаемого уровня параллелизма. Функция run принимает массив заданий и желаемое количество одновременно работающих исполнителей. Он делит задания на фрагменты и назначает их отдельным рабочим потокам.
Каждый рабочий поток запускает отдельный файл JavaScript (worker.js) и обрабатывает назначенный ему фрагмент данных. По завершении сообщение отправляется обратно в основной поток, который отслеживает количество завершенных воркеров. Как только все исполнители завершат свои задачи, основной поток измеряет затраченное время с помощью performance.now() и записывает результат.
Вот результаты с разными одновременно работающими работниками.
Main thread took 97455.16140002012ms 2 workers took 54371.36129999161ms 4 workers took 25974.414699971676ms 8 workers took 18283.357300043106ms 16 workers took 18525.872299969196ms
Результаты, полученные с разным количеством одновременно работающих работников, показывают, как производительность улучшается по мере увеличения параллелизма. Однако есть момент, когда добавление большего количества рабочих процессов не приводит к дальнейшему увеличению производительности из-за ограничений базового оборудования. Это наблюдение особенно очевидно в случае упомянутого процессора, имеющего всего 8 ядер. Следовательно, время, затраченное 8 и 16 работниками, одинаково из-за мощности процессора.
Заключение
В заключение отметим, что JavaScript, известный своей однопоточной природой, нашел инновационные способы использования возможностей параллелизма с помощью таких инструментов, как рабочие потоки Node.js и веб-воркеры браузера. Тщательно распределяя задачи по нескольким потокам, разработчики могут повысить производительность и скорость реагирования своих приложений.
Мы исследовали переход от однопоточной парадигмы к многопоточному подходу с использованием рабочих потоков Node.js, воочию став свидетелями прироста производительности, достигнутого за счет одновременного выполнения. Однако важно помнить, что хотя многопоточность может значительно повысить производительность, не менее важно найти баланс между параллелизмом и доступными аппаратными ресурсами.
Поскольку JavaScript продолжает развиваться, использование многопоточных возможностей может проложить путь к созданию более сложных и эффективных веб-приложений.
Спасибо!