Когда используется пул потоков?

104

Итак, у меня есть понимание того, как работает Node.js: у него есть единственный поток слушателя, который получает событие, а затем делегирует его рабочему пулу. Рабочий поток уведомляет слушателя после завершения работы, а затем слушатель возвращает ответ вызывающей стороне.

Мой вопрос таков: если я включаю HTTP-сервер в Node.js и вызываю sleep для одного из событий маршрутизации (например, «/ test / sleep»), вся система останавливается. Даже один поток слушателя. Но я понял, что этот код происходит в рабочем пуле.

Теперь же, когда я использую Mongoose для общения с MongoDB, чтение из БД - дорогостоящая операция ввода-вывода. Кажется, что Node может делегировать работу потоку и получить обратный вызов по завершении; время, затраченное на загрузку из БД, похоже, не блокирует систему.

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

Хейни
источник
@Tobi - я это видел. Он все еще не отвечает на мой вопрос. Если бы работа была в другом потоке, сон повлиял бы только на этот поток, а не на слушателя.
Haney
8
Настоящий вопрос, когда вы пытаетесь что-то понять самостоятельно, а когда не можете найти выход в лабиринт, вы просите о помощи.
Рафаэль Эйнг

Ответы:

242

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

На данный момент мы проигнорируем явную многопроцессорность / многопоточность через кластер и потоки webworker и просто поговорим о типичном непоточном узле.

Узел работает в одном цикле событий. Он однопоточный, и вы всегда получаете только этот один поток. Весь написанный вами javascript выполняется в этом цикле, и если в этом коде происходит операция блокировки, то он блокирует весь цикл, и больше ничего не произойдет, пока он не завершится. Это типичная однопоточная природа узла, о которой вы так много слышите. Но это еще не вся картина.

Некоторые функции и модули, обычно написанные на C / C ++, поддерживают асинхронный ввод-вывод. Когда вы вызываете эти функции и методы, они внутренне управляют передачей вызова рабочему потоку. Например, когда вы используете fsмодуль для запроса файла, fsмодуль передает этот вызов рабочему потоку, и этот рабочий ждет своего ответа, который затем отправляет обратно в цикл событий, который продолжался без него в тем временем. Все это абстрагируется от вас, разработчика узла, а некоторая часть абстрагируется от разработчиков модулей с помощью libuv .

Как указал Денис Доллфус в комментариях (из этого ответа на аналогичный вопрос), стратегия, используемая libuv для достижения асинхронного ввода-вывода, не всегда является пулом потоков, в частности, в случае httpмодуля, другая стратегия кажется используется в это время. Для наших целей здесь в основном важно отметить, как достигается асинхронный контекст (с помощью libuv) и что пул потоков, поддерживаемый libuv, является одной из нескольких стратегий, предлагаемых этой библиотекой для достижения асинхронности.


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

  • Любой внешний модуль, который вы включаете в свой проект, который использует собственный C ++ и libuv, вероятно, будет использовать пул потоков (подумайте: доступ к базе данных)
  • libuv имеет размер пула потоков по умолчанию, равный 4, и использует очередь для управления доступом к пулу потоков - результат состоит в том, что если у вас есть 5 длительно выполняемых запросов к базе данных, все выполняются одновременно, один из них (и любые другие асинхронные действие, которое полагается на пул потоков) будет ждать завершения этих запросов еще до того, как они начнутся
  • Вы можете смягчить это, увеличив размер пула потоков с помощью UV_THREADPOOL_SIZEпеременной среды, если вы делаете это до того, как пул потоков потребуется и будет создан:process.env.UV_THREADPOOL_SIZE = 10;

Если вам нужна традиционная многопроцессорная обработка или многопоточность в узле, вы можете получить это через встроенный clusterмодуль или различные другие модули, такие как вышеупомянутые webworker-threads, или вы можете подделать его, реализовав какой-либо способ разбивки своей работы и вручную используя setTimeoutили setImmediateили process.nextTickприостановить вашу работу и продолжить ее в более позднем цикле, чтобы позволить другим процессам завершиться (но это не рекомендуется).

Обратите внимание: если вы пишете долго работающий / блокирующий код на javascript, вы, вероятно, ошибаетесь. Другие языки будут работать намного эффективнее.

Джейсон
источник
1
Черт возьми, это полностью проясняет мне ситуацию. Большое спасибо, @Jason!
Haney
5
Нет проблем :) Я оказался там, где вы не так давно, и было сложно прийти к четко определенному ответу, потому что с одной стороны у вас есть разработчики C / C ++, для которых ответ очевиден, а с другой - типичный веб-разработчикам, которые раньше не вникали слишком глубоко в подобные вопросы. Я даже не уверен, что мой ответ на 100% технически верен, когда вы переходите к уровню C, но в общих чертах он верен.
Джейсон
3
Использование пула потоков для сетевых запросов было бы огромной тратой ресурсов. Согласно этому вопросу «Он выполняет асинхронный сетевой ввод-вывод на основе интерфейсов асинхронного ввода-вывода на различных платформах, таких как epoll, kqueue и IOCP, без пула потоков», что имеет смысл.
Денис Дольфус
1
... при этом, если вы выполняете тяжелую работу в основном потоке javascript напрямую, или у вас недостаточно ресурсов или вы не управляете ими должным образом, чтобы дать достаточно места для пула потоков, вы можете ввести задержку при более низком параллелизме порог - в результате для одних и тех же системных ресурсов вы, как правило, будете испытывать более высокую пропускную способность с node.js, чем с другими вариантами (хотя есть другие системы на основе событий на других языках, которые стремятся оспорить это - я не видел недавние тесты) - ясно, что модель, основанная на событиях, превосходит многопоточную модель.
Джейсон
1
@Aabid Поток слушателя не выполняет запрос к базе данных, поэтому выполнение всех 10 этих запросов займет примерно 6 секунд (по умолчанию размер пула потоков равен 4). Если вам нужно выполнить какую-либо работу в javascript, которая не требует завершения результатов этого запроса к базе данных, например, поступает больше запросов, которые не требуют выполнения какой-либо асинхронной работы пулом потоков, он будет продолжать работать в основном цикл событий.
Джейсон
20

Итак, у меня есть понимание того, как работает Node.js: у него есть единственный поток слушателя, который получает событие, а затем делегирует его рабочему пулу. Рабочий поток уведомляет слушателя после завершения работы, а затем слушатель возвращает ответ вызывающей стороне.

Это не совсем так. Node.js имеет только один «рабочий» поток, который выполняет javascript. Внутри узла есть потоки, которые обрабатывают обработку ввода-вывода, но думать о них как о «рабочих» - неправильное представление. На самом деле есть просто обработка ввода-вывода и несколько других деталей внутренней реализации узла, но как программист вы не можете влиять на их поведение, кроме нескольких разных параметров, таких как MAX_LISTENERS.

Мой вопрос таков: если я включаю HTTP-сервер в Node.js и вызываю sleep для одного из событий маршрутизации (например, «/ test / sleep»), вся система останавливается. Даже один поток слушателя. Но я понял, что этот код происходит в рабочем пуле.

В JavaScript нет механизма сна. Мы могли бы обсудить это более конкретно, если бы вы опубликовали фрагмент кода, который, по вашему мнению, означает «сон». Нет такой функции, которую можно было бы вызвать, например, для имитации чего-то вроде time.sleep(30)Python. Там же , setTimeoutно это в корне не сон. setTimeoutи setIntervalявно освобождает , а не блокирует, цикл событий, чтобы другие части кода могли выполняться в основном потоке выполнения. Единственное, что вы можете сделать, - это зациклить CPU с вычислениями в памяти, что действительно приведет к голоданию основного потока выполнения и сделает вашу программу невосприимчивой.

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

Сетевой ввод-вывод всегда асинхронный. Конец истории. Disk IO имеет как синхронные, так и асинхронные API, поэтому «решения» нет. node.js будет вести себя в соответствии с основными функциями API, которые вы вызываете синхронизацией по сравнению с обычным асинхронным. Например: fs.readFileпротив fs.readFileSync. Для дочерних процессов, существуют также отдельные child_process.execи child_process.execSyncAPI - интерфейсы.

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

Питер Лайонс
источник
1
Откуда берутся эти асинхронные API? Я понимаю, о чем вы говорите, но тот, кто написал эти API, выбрал IOCP / async. Как они решили это сделать?
Haney
3
Его вопрос в том, как он мог бы написать свой собственный ресурсоемкий код, а не блокировать.
Джейсон
1
Да. Узел обеспечивает базовую сеть UDP, TCP и HTTP. Он предоставляет ТОЛЬКО асинхронные API на основе «пула». Весь код node.js в мире без исключения использует эти асинхронные API на основе пула, так как есть просто все, что доступно. Файловая система и дочерние процессы - это совсем другая история, но сеть всегда асинхронна.
Питер Лайонс
4
Осторожно, Питер, иначе ты будешь пресловутым горшком для его чайника. Он хочет знать, как это сделали авторы сетевого API, а не как это делают люди, использующие сетевой API. В конце концов я понял, как узел ведет себя в отношении неблокирующих событий, потому что я хотел написать свой собственный неблокирующий код, который не имеет ничего общего с сетью или какими-либо другими встроенными асинхронными API. Совершенно очевидно, что Дэвид хочет сделать то же самое.
Джейсон
2
Node не использует пулы потоков для ввода-вывода, он использует собственный неблокирующий ввод-вывод, единственное исключение fs, насколько мне известно,
vkurchatkin
2

Пул потоков, как когда и кто использовал:

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

введите описание изображения здесь

Таким образом, механизм однопоточности позволяет легко заблокировать приложение узла, но это одна из уникальных функций, которые Node.js привносит в таблицу. Итак, опять же, если вы запустите приложение узла, оно будет работать только в одном потоке. Независимо от того, есть ли у вас 1 или миллион пользователей, одновременно обращающихся к вашему приложению.

Итак, давайте точно поймем, что происходит в единственном потоке nodejs, когда вы запускаете свое приложение node. Сначала инициализируется программа, затем выполняется весь код верхнего уровня, что означает все коды, которые не находятся внутри какой-либо функции обратного вызова ( помните, что все коды внутри всех функций обратного вызова будут выполняться в цикле событий ).

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

введите описание изображения здесь

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

Что ж, для лучшего понимания пула потоков я прошу вас представить, что в цикле событий коды внутри одной функции обратного вызова выполняются после завершения выполнения кодов внутри другой функции обратного вызова, теперь, если есть некоторые задачи, на самом деле слишком тяжелые. Затем они заблокировали бы наш единственный поток nodejs. Итак, здесь появляется пул потоков, который, как и цикл событий, предоставляется Node.js библиотекой libuv.

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

введите описание изображения здесь

Пул потоков дает нам четыре дополнительных потока, которые полностью отделены от основного потока. И мы действительно можем настроить его до 128 потоков.

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

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

В пул потоков поступает много задач, например

-> All operations dealing with files
->Everyting is related to cryptography, like caching passwords.
->All compression stuff
->DNS lookups
Лорд
источник
0

Это недоразумение - просто разница между упреждающей многозадачностью и совместной многозадачностью ...

Сон отключает весь карнавал, потому что на самом деле все аттракционы проходят в одну линию, и вы закрыли ворота. Думайте об этом как о «интерпретаторе JS и некоторых других вещах» и игнорируйте потоки ... для вас есть только один поток, ...

... так что не блокируйте это.

Грегори Р. Саддерт
источник