Как использовать каждое ядро ​​на вашем компьютере с помощью NodeJS

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

Задний план

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

Долгое время JavaScript и NodeJS были ограничены циклом событий. Код выполняется асинхронно, но не параллельно. Однако это изменилось с выпуском рабочих потоков в NodeJS.

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

Цель

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

Проблема

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

Приведенный выше код неверен, потому что он не учитывает некоторые распространенные крайние случаи:

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

Последний вопрос - гвоздь в гроб. Если для обработки большинства заданий требуется 2 секунды, а для одного требуется 3 часа, то весь пул должен ждать 3 часа, пока все рабочие не будут освобождены.

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

Начальные решения

Поскольку Promise.all блокирует, я сразу подумал, что Promise.any или Promise.race должны быть ответом на истинный параллелизм, но я ошибался. На самом деле, одних Promise методов недостаточно для многозадачности.

Итак, решено, Promise.race, вероятно, решение, а Promise.any ошибочно, потому что Promise.any должен успешно выполнить хотя бы одно обещание или ждать, пока все не выполнятся.

Что произойдет, если все задания выйдут из строя, кроме одного, на которое уходит 3 часа? Опять же, весь пул должен подождать 3 часа, прежде чем задание завершится или вызовет Aggregate Error.

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

Работа держит нить

Решение проблемы Promise.race - это сами рабочие. Не имеет значения, когда обещание выполняется, потому что рабочий работает в фоновом режиме.

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

Остановка

Последняя цель - остановить выполнение всего пула. Даже если выполняется трехчасовое задание, оно немедленно останавливается. Честно говоря, в этом разобраться сложнее, чем в других проблемах с обещаниями.

Мой первый инстинкт - отказаться от обещания, но это проблематично. Я заметил, что прохождение reasons через вызов reject означало, что Promise.race может разрешить только один reason. Тем не менее, обещание всех причин возвращает меня к чертежной доске.

Хуже того, отклонение обещания позволяет завершить цикл основного события, но рабочие превращаются в зомби! Через 3 часа - рабочий вывод все еще забивает ваш терминал!

К счастью, я сделал открытие. Потоки должны явно завершать работника. Это делает процесс завершения полностью детерминированным, что исключает компрометацию данных. Обещание разрешается после того, как заканчивается гонка за обещаниями о работе.

Успех проекта!

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

npm install jpool

Функции

Количество потоков варьируется, и все состояния детерминированы. Задание будет выполнено, завершится ошибкой или остановится. Это позволяет пулу изящно завершать работу или внезапно завершать работу без зомби или неконтролируемых процессов.

Базовый пример (Main.js)

Продолж. Пример (Job.js)

Увидеть разницу!

Каждое окно терминала обрабатывает один и тот же набор заданий. Слева направо программы используют 1, 8 и 256 рабочих. Потоки увеличивают использование памяти, но преимущества того стоят!

Конец

Документация требует доработки, в противном случае пакет кажется стабильным для версии 1.0.0. Если вы хотите помочь, я принимаю PR. Спасибо за чтение!