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 включает множество примитивов для координации задач:
task(): Создает задачу, которая может быть отменена.WaitGroup/ErrGroup: Группы ожидания для синхронизации задач.select(): Выбор из нескольких каналов.context: Контекст для управления жизненным циклом задач.Mutex/RWMutex/Cond: Примитивы для синхронизации.Timer/Ticker: Таймеры и интервальные таймеры.
Пример использования 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