puru - JavaScript библиотека для конкурентности: worker threads, каналы и структурированная конкурентность

#JavaScript#concurrency#worker threads#structured concurrency

TL;DR:
Библиотека puru заполняет пробел между Promise.all() и низкоуровневыми worker_threads в JavaScript, предоставляя удобные абстракции для работы с CPU-bound задачами, каналами и структурированной конкурентностью. Она упрощает создание и управление worker threads, минимизируя boilerplate код.

Введение

В современном JavaScript асинхронный I/O реализован достаточно хорошо, но когда речь заходит о CPU-bound задачах и структурированной конкурентности, разработчики сталкиваются с рядом сложностей. Использование worker_threads часто требует создания отдельных файлов для воркеров, ручного управления сообщениями и жизненным циклом, что приводит к большому количеству boilerplate кода.

Библиотека puru призвана упростить эти паттерны, сохраняя при этом явную модель работы с воркерами. Она предоставляет набор примитивов для координации задач, таких как каналы, группы ожидания, мьютексы и таймеры, что делает работу с конкурентностью более выразительной и удобной.

Основная часть

Базовый пример использования spawn

Одна из ключевых функций puru — это spawn, который позволяет запускать CPU-bound задачи в отдельном воркере. Рассмотрим пример вычисления числа Фибоначчи:

import { spawn } from '@dmop/puru';

const { result } = spawn(() => {
  function fibonacci(n: number): number {
    if (n <= 1) return n;
    return fibonacci(n - 1) + fibonacci(n - 2);
  }
  return fibonacci(40);
});

console.log(await result); // Вывод: 102334155

Здесь функция fibonacci выполняется в отдельном воркере, что позволяет не блокировать основной поток. Результат вычисления возвращается через промис result.

Каналы и пайплайны

puru предоставляет абстракцию каналов (chan), которая упрощает передачу данных между воркерами. Рассмотрим пример пайплайна, где каждый воркер умножает входное число на 2:

import { chan, spawn } from '@dmop/puru';

const input = chan<number>(50);
const output = chan<number>(50);

for (let i = 0; i < 4; i++) {
  spawn(async ({ input, output }) => {
    for await (const n of input) {
      await output.send(n * 2);
    }
  }, { channels: { input, output } });
}

// Отправка данных в канал
input.send(1);
input.send(2);
input.send(3);
input.close();

// Чтение результатов
for await (const result of output) {
  console.log(result); // Вывод: 2, 4, 6
}

Здесь четыре воркера параллельно обрабатывают данные из канала input и отправляют результаты в канал output.

Примитивы для координации

puru включает множество примитивов для координации задач:

Пример использования WaitGroup:

import { WaitGroup, spawn } from '@dmop/puru';

const wg = new WaitGroup();

wg.add(2);

spawn(() => {
  // Выполнение задачи
  wg.done();
});

spawn(() => {
  // Выполнение другой задачи
  wg.done();
});

await wg.wait(); // Ожидание завершения всех задач

Практическое применение

puru особенно полезна в сценариях, где требуется выполнение CPU-bound задач, таких как обработка изображений, машинное обучение или сложные вычисления. Она также упрощает создание конвейеров данных, где задачи могут быть параллелизованы и синхронизированы с помощью каналов.

Одним из осознанных решений в puru является то, что функции, передаваемые в spawn, сериализуются и отправляются в воркер, что означает, что они не могут захватывать внешние переменные. Это сделано для того, чтобы сохранить явность модели и избежать скрытых ошибок.

Заключение

Библиотека puru предлагает мощные и удобные абстракции для работы с конкурентностью в JavaScript. Она особенно полезна для разработчиков, которые работают с CPU-bound задачами, пайплайнами и структурированной конкурентностью. Если вы сталкиваетесь с подобными задачами, puru может стать отличным инструментом для упрощения вашего кода и повышения его производительности.

GitHub: https://github.com/dmop/puru


Источник: https://github.com/dmop/puru