Node js многопоточность: Многопоточность в Node.js
Многопоточность на Node.js. Event Loop / Хабр
Инфа будет полезна JS-разработчикам, которые хотят глубоко понимать суть работы с Node.js и Event Loop. Вы сможете осознанно и более гибко управлять потоком выполнения программы (web-сервера).
Эту статью я составил по материалам своего недавнего доклада для коллег.
В конце статьи есть полезные материалы для самостоятельного изучения.
Как устроена Node.js. Возможности асинхрона
Давайте посмотрим на этот код: он отлично демонстрирует синхронность выполнения кода в Node.js. Делается запрос на GitHub, затем записываем данные в файл и выводим результат в консоли. Что понятно из этого синхронного кода?
Предположим, что это абстрактный web-сервер, который по какому-то роутеру делает операции. Если по данному роутеру приходит входящий запрос, мы делаем дальше запрос, читаем файл, выводим в консоль. Соответственно, то время, которое тратится на запрос и чтение файла, сервер будет заблокирован, никакие другие входящие запросы он обрабатывать не сможет, выполнять другие операции — тоже.
Какие есть варианты решения данной проблемы?
- Многопоточность
- Неблокирующий ввод/вывод
Для первого варианта (многопоточность) есть хороший пример с web-сервером Apache vs Nginx.
Раньше Apache под каждый входящий запрос поднимал поток: сколько было запросов, столько же было и потоков. В это время Nginx имел преимущество, т. к. использовал неблокирующий ввод/вывод. Здесь вы можете видеть, что с увеличением количества входящих запросов увеличивается объём потребляемой памяти именно у Apache, а на следующем слайде количество обрабатываемых запросов в секунду с количеством подключений у Nginx выше.
Наглядно показано, что неблокирующий ввод/вывод эффективнее.
Неблокирующий ввод/вывод стал возможным благодаря современным операционным системам, которые предоставляют данный механизм — демультиплексор событий.
Демультиплексор — это механизм, который принимает от приложения запрос, регистрирует его и выполняет.
В верхней части схемы видно, что у нас есть приложение и в нём выполняются операции (пусть это будет чтение файла). Для этого делается запрос в демультиплексор событий, сюда отправляется ресурс (ссылка на файл), нужная операция и callback. Демультиплексор событий регистрирует этот запрос и возвращает управление непосредственно приложению — таким образом, оно не блокируется. Затем он выполняет операции над файлом, и после этого, когда файл будет прочитан, callback регистрируется в очереди на выполнение. Затем Event Loop постепенно синхронно обрабатывает каждый callback из этой очереди. И, соответственно, возвращает результат приложению. Дальше (если необходимо) всё делается снова.
Таким образом, благодаря данному неблокирующему вводу/выводу Node.js может быть асинхронным.
Уточню, что в данном случае неблокирующий ввод/вывод предоставляет нам именно операционная система. К неблокирующему вводу/выводу (вообще в принципе к операциям ввода/вывода) мы относим сетевые запросы и работу с файлами.
Это общая концепция неблокирующего ввода/вывода. Когда такая возможность появилась, Райан Дал (Ryan Dahl) — разработчик Node.js — был вдохновлён опытом Nginx, которая использовала неблокирующий ввод/вывод, и решил создать платформу именно для разработчиков. Первое, что ему нужно было сделать, — «подружить» свою платформу с демультиплексором событий. Проблема была в том, что в каждой операционной системе демультиплексор реализован по-разному, и ему пришлось написать обёртку, которая впоследствии стала называться libuv. Это библиотека, написанная на C. Она предоставляет единый интерфейс работы с этими демультиплексорами событий.
Особенности libuv-библиотеки
В Linux, в принципе, на текущий момент все операции с локальными файлами — блокирующие. Т. е. вроде бы как есть неблокирующий ввод/вывод, но именно при работе с локальными файлами операция всё равно блокирующая. Именно поэтому для эмуляции неблокирующего ввода/вывода libuv использует внутри потоки. Из коробки поднимается 4 потока, и здесь нужно сделать самый важный вывод: если мы выполняем какие-то 4 тяжёлые операции над локальными файлами, соответственно, мы заблокируем всё наше приложение (именно в ОС Linux, в других ОС такого нет).
На этом слайде мы видим архитектуру Node.js. Для взаимодействия с операционной системой используется библиотека libuv, написанная на C; для компиляции кода JavaScript’a в машинный код используется движок Google V8, также есть Node.js Core library, где собраны модули для работы с сетевыми запросами, файловой системой и модуль для логирования. Чтобы всё это друг с другом взаимодействовало, написаны Node.js Bindings. Эти 4 компонента составляют саму структуру Node.js. Сам механизм Event Loop’a находится в libuv.
Event Loop
Это простейшее представление, как выглядит Event Loop. Есть определённая очередь событий, есть бесконечный цикл событий, который синхронно выполняет операции из очереди, и он распределяет их дальше.
На этом слайде показано, как выглядит Event Loop непосредственно в самой Node.js.
Там реализация поинтереснее и посложнее. По сути, Event Loop — это цикл событий, и он бесконечен до тех пор, пока есть что выполнять. Event Loop в Node.js делится на несколько фаз. (Фазы со слайда 8 надо сопоставлять с исходным кодом на слайде 9.)
1 фаза — таймеры
Данная фаза выполняется непосредственно Event Loop’ом. (Фрагмент кода с uv_update_time) — здесь просто обновляется время, когда начал работать Event Loop.
uv_run_timers — в этом методе выполняется следующее действие с таймером. Есть определённый стек, точнее, куча таймеров, это, по сути, то же самое, что очередь, где находятся таймеры. Берётся таймер с самым маленьким временем, сравнивается с текущим времени Event Loop’а, и, если настало время для исполнения данного таймера, выполняется его callback. Здесь стоит отметить что в Node.js есть реализация setTimeout и есть setInterval. Для libuv это, по сути, одно и то же, только в setInterval ещё есть флаг repeat.
Соответственно, если у данного таймера стоит флаг repeat, то он снова помещается в очередь событий и потом точно так же обрабатывается.
2 фаза — I/O-callback’и
Здесь надо вернуться к диаграмме про неблокирующий ввод/вывод.
Когда демультиплексор событий выполняет чтение какого-либо файла и ставит выполнение callback’а в очередь, это как раз соответствует этапу I/O-callback. Здесь выполняются callback’и для неблокирующего ввода/вывода, т. е. это именно те функции, которые используются после запроса в базу данных или другой ресурс или на чтение/запись файла. Они выполняются именно на данной фазе.
На слайде 9 выполнение функции I/O-callback’и запускает строка 367: ran_pending = uv_run_pending(loop).
3 фаза — ожидание, подготовка
Это внутренние операции для callback’ов, по сути, мы не можем влиять на фазу, только косвенно. Есть process.nextTick, его callback может ненамеренно быть исполнен на фазе «ожидание, подготовка». process.nextTick выполняется на текущей фазе, т. е., по сути, process.nextTick может сработать абсолютно на любой фазе. Какого-то готового инструмента, чтобы запустить код на фазе «ожидание, подготовка», в Node.js нет.
На слайде 9 этой фазе соответствуют строки 368, 369:
uv_run_idle(loop) — ожидание;
uv_run_prepare(loop) — подготовка.
4 фаза — опрос
Здесь выполняется весь наш код, который мы пишем на JS. Первоначально все запросы, которые мы делаем, попадают именно сюда, и именно здесь Node.js может быть заблокирована. Если сюда попадёт какая-либо тяжёлая операция по вычислению, то на этом этапе наше приложение может просто зависнуть и ожидать, пока не выполнится данная операция.
На слайде 9 функция опроса содержится в строке 370: uv_io_poll(loop, timeout).
5 фаза — проверка
В Node.js есть таймер setImmediate, его callback’и выполняются на этой фазе.
В исходном коде это строка 371: uv_run_check(loop).
6 фаза (последняя) — callback’и событий close
Например, web-сокету нужно закрыть соединение, на этой фазе будет вызван callback этого события.
В исходном коде это строка 372: uv_run_closing_handless(loop).
И в итоге Event Loop Node.js выглядит следующим образом
Сначала в очереди таймеров выполняется тот таймер, срок которого подошёл.
Дальше выполняются I/O-callback’и.
Затем — основой код, дальше — setImmediate и события close.
После этого всё повторяется по кругу. Чтобы продемонстрировать это, открою код. Как он будет выполняться?
У нас нет таймеров в очереди, поэтому Event Loop двигается дальше. I/O-callback’ов тоже нет, поэтому идём сразу на фазу опроса. Весь код, который здесь есть, изначально выполняется на фазе опроса. Поэтому сначала печатаем script_start, setInterval у нас помещается в очередь таймеров (не выполняется, просто помещается). setTimeout также помещается в очередь таймеров, и затем выполняются промисы: сначала promise 1 и затем promise 2.
В следующий тик (цикл событий) мы возвращаемся на этап таймеров, здесь в очереди уже есть 2 таймера: setInterval и setTimeout. Они оба с задержкой 0, соответственно, они готовы к выполнению.
Выполняются (выводятся в консоль) setInterval, затем setTimeout 1. Callback’ов неблокирующего ввода/вывода нет, дальше будет фаза опроса, в консоль выводятся promise 3 и promise 4.
Дальше регистрируется таймер setTimeout. На этом тик заканчивается, идём в следующий тик. Там — снова таймеры, выполняется вывод в консоль setInterval и setTimeout 2, затем выводятся promise 5 и promise 6.
Event Loop мы рассмотрели и теперь можем более подробно поговорить о многопоточности.
Многопоточность — модуль worker_threads
Многопоточность появилась в Node.js благодаря модулю worker_threads в версии 10.5. И в 10-й версии она запускалась исключительно с ключом —experimental-worker, а с 11-й версии стал возможным запуск без него.
Ещё в Node.js есть модуль cluster, но он не поднимает потоки — он поднимает ещё несколько процессов. Масштабируемость приложения — его основная цель.
Как вообще выглядит 1 процесс:
1 процесс Node.js, 1 поток, 1 Event Loop, 1 движок V8 и libuv.
Если мы запускаем X потоков, то у нас это выглядит так:
1 процесс Node.js, X потоков, Х Event Loop’ов, X движков V8 и X libuv.
Схематично это выглядит следующим образом
Давайте разберём пример.
Простейший web-сервер на Express’е. Есть 2 route’а — / и /fat-operation.
Также есть функция generateRandomArr(). Она наполняет массив двумя миллионами записей и сортирует его. Запустим сервер.
Делаем запрос на /fat-operation. И в тот момент, когда выполняется операция сортировки массива, отправляем ещё один запрос на route /, но для получения ответа нам приходится ждать до тех пор, пока не выполнится сортировка массива. Это классическая реализация через один поток. Теперь подключаем модуль worker_threads.
Делаем запрос на /fat-operation и следом — на /, от которого тут же получаем ответ — Hello world!
Для операции сортировки массива мы подняли отдельный поток, у которого есть свой экземпляр Event Loop, и он никак не влияет на выполнение кода в основном потоке.
Поток будет «уничтожен», когда у него не будет операций для выполнения.
Смотрим исходный код. Регистрируем worker в строке 26 и, если нужно, передаём ему данные. В данном случае я ничего не передаю. И затем подписываемся на события: на ошибку и на месседж. В самом worker’е происходит вызов функции, массив из двух миллионов записей сортируется. Как только он отсортировался, мы через post_message отправляем результат в основной поток ok.
В основном потоке мы ловим это сообщение и отправляем результат finish. У worker’а и основного потока общая память, поэтому мы имеем доступ к глобальным переменным всего процесса. Когда мы передаём данные из основного потока в worker, worker получает только копию.
Основной поток и поток worker’а мы можем описать в одном файле. Модуль worker_threads предоставляет API, благодаря которому мы можем определить, в каком потоке сейчас выполняется код.
Дополнительная информация
Делюсь ссылками на полезные ресурсы и ссылкой на презентацию Райана Дала, когда он презентовал Event Loop (интересно посмотреть).
Event Loop
- Перевод статьи из документации Node. js
- https://blog.risingstack.com/node-js-at-scale-understanding-node-js-event-loop/
- https://habr.com/ru/post/336498/
Worker_threads
- https://nodejs.org/api/worker_threads.html#worker_threads_worker_workerdata — API
- https://habr.com/ru/company/ruvds/blog/415659/
- https://nodesource.com/blog/worker-threads-nodejs/
- Original slides from Ryan Dahl’s presentation (through VPN)
Многопоточность на Node.js. Event Loop
Инфа будет полезна JS-разработчикам, которые хотят глубоко понимать суть работы с Node.js и Event Loop. Вы сможете осознанно и более гибко управлять потоком выполнения программы (web-сервера).
Эту статью я составил по материалам своего недавнего доклада для коллег.
В конце статьи есть полезные материалы для самостоятельного изучения.
Как устроена Node.js. Возможности асинхрона
Давайте посмотрим на этот код: он отлично демонстрирует синхронность выполнения кода в Node. js. Делается запрос куда-то на GitHub, затем читается файл и выводится результат в консоли. Что понятно из этого синхронного кода?
Предположим, что это абстрактный web-сервер, который по какому-то роутеру делает операции. Если по данному роутеру приходит входящий запрос, мы делаем дальше запрос, читаем файл, выводим в консоль. Соответственно, то время, которое тратится на запрос и чтение файла, сервер будет заблокирован, никакие другие входящие запросы он обрабатывать не сможет, выполнять другие операции — тоже.
Какие есть варианты решения данной проблемы?
- Многопоточность
- Неблокирующий ввод/вывод
Для первого варианта (многопоточность) есть хороший пример с web-сервером Apache vs Nginx.
Раньше Apache под каждый входящий запрос поднимал поток: сколько было запросов, столько же было и потоков. В это время Nginx имел преимущество, т. к. использовал неблокирующий ввод/вывод. Здесь вы можете видеть, что с увеличением количества входящих запросов увеличивается объём потребляемой памяти именно у Apache, а на следующем слайде количество обрабатываемых запросов в секунду с количеством подключений у Nginx выше.
Наглядно показано, что неблокирующий ввод/вывод эффективнее.
Неблокирующий ввод/вывод стал возможным благодаря современным операционным системам, которые предоставляют данный механизм — демультиплексор событий.
Демультиплексор — это механизм, который принимает от приложения запрос, регистрирует его и выполняет.
В верхней части схемы видно, что у нас есть приложение и в нём выполняются операции (пусть это будет чтение файла). Для этого делается запрос в демультиплексор событий, сюда отправляется ресурс (ссылка на файл), нужная операция и callback. Демультиплексор событий регистрирует этот запрос и возвращает управление непосредственно приложению — таким образом, оно не блокируется. Затем он выполняет операции над файлом, и после этого, когда файл будет прочитан, callback регистрируется в очереди на выполнение. Затем Event Loop постепенно синхронно обрабатывает каждый callback из этой очереди. И, соответственно, возвращает результат приложению. Дальше (если необходимо) всё делается снова.
Таким образом, благодаря данному неблокирующему вводу/выводу Node.js может быть асинхронным.
Уточню, что в данном случае неблокирующий ввод/вывод предоставляет нам именно операционная система. К неблокирующему вводу/выводу (вообще в принципе к операциям ввода/вывода) мы относим сетевые запросы и работу с файлами.
Это общая концепция неблокирующего ввода/вывода. Когда такая возможность появилась, Райан Дал (Ryan Dahl) — разработчик Node.js — был вдохновлён опытом Nginx, которая использовала неблокирующий ввод/вывод, и решил создать платформу именно для разработчиков. Первое, что ему нужно было сделать, — «подружить» свою платформу с демультиплексором событий. Проблема была в том, что в каждой операционной системе демультиплексор реализован по-разному, и ему пришлось написать обёртку, которая впоследствии стала называться libuv. Это библиотека, написанная на C. Она предоставляет единый интерфейс работы с этими демультиплексорами событий.
Особенности libuv-библиотеки
В Linux, в принципе, на текущий момент все операции с локальными файлами — блокирующие. Т. е. вроде бы как есть неблокирующий ввод/вывод, но именно при работе с локальными файлами операция всё равно блокирующая. Именно поэтому для эмуляции неблокирующего ввода/вывода libuv использует внутри потоки. Из коробки поднимается 4 потока, и здесь нужно сделать самый важный вывод: если мы выполняем какие-то 4 тяжёлые операции над локальными файлами, соответственно, мы заблокируем всё наше приложение (именно в ОС Linux, в других ОС такого нет).
На этом слайде мы видим архитектуру Node.js. Для взаимодействия с операционной системой используется библиотека libuv, написанная на C; для компиляции кода JavaScript’a в машинный код используется движок Google V8, также есть Node.js Core library, где собраны модули для работы с сетевыми запросами, файловой системой и модуль для логирования. Чтобы всё это друг с другом взаимодействовало, написаны Node. js Bindings. Эти 4 компонента составляют саму структуру Node.js. Сам механизм Event Loop’a находится в libuv.
Event Loop
Это простейшее представление, как выглядит Event Loop. Есть определённая очередь событий, есть бесконечный цикл событий, который синхронно выполняет операции из очереди, и он распределяет их дальше.
На этом слайде показано, как выглядит Event Loop непосредственно в самой Node.js.
Там реализация поинтереснее и посложнее. По сути, Event Loop — это цикл событий, и он бесконечен до тех пор, пока есть что выполнять. Event Loop в Node.js делится на несколько фаз. (Фазы со слайда 8 надо сопоставлять с исходным кодом на слайде 9.)
1 фаза — таймеры
Данная фаза выполняется непосредственно Event Loop’ом. (Фрагмент кода с uv_update_time) — здесь просто обновляется время, когда начал работать Event Loop.
uv_run_timers — в этом методе выполняется следующее действие с таймером. Есть определённый стек, точнее, куча таймеров, это, по сути, то же самое, что очередь, где находятся таймеры. Берётся таймер с самым маленьким временем, сравнивается с текущим времени Event Loop’а, и, если настало время для исполнения данного таймера, выполняется его callback. Здесь стоит отметить что в Node.js есть реализация setTimeout и есть setInterval. Для libuv это, по сути, одно и то же, только в setInterval ещё есть флаг repeat.
Соответственно, если у данного таймера стоит флаг repeat, то он снова помещается в очередь событий и потом точно так же обрабатывается.
2 фаза — I/O-callback’и
Здесь надо вернуться к диаграмме про неблокирующий ввод/вывод.
Когда демультиплексор событий выполняет чтение какого-либо файла и ставит выполнение callback’а в очередь, это как раз соответствует этапу I/O-callback. Здесь выполняются callback’и для неблокирующего ввода/вывода, т. е. это именно те функции, которые используются после запроса в базу данных или другой ресурс или на чтение/запись файла. Они выполняются именно на данной фазе.
На слайде 9 выполнение функции I/O-callback’и запускает строка 367: ran_pending = uv_run_pending(loop).
3 фаза — ожидание, подготовка
Это внутренние операции для callback’ов, по сути, мы не можем влиять на фазу, только косвенно. Есть process.nextTick, его callback может ненамеренно быть исполнен на фазе «ожидание, подготовка». process.nextTick выполняется на текущей фазе, т. е., по сути, process.nextTick может сработать абсолютно на любой фазе. Какого-то готового инструмента, чтобы запустить код на фазе «ожидание, подготовка», в Node.js нет.
На слайде 9 этой фазе соответствуют строки 368, 369:
uv_run_idle(loop) — ожидание;
uv_run_prepare(loop) — подготовка.
4 фаза — опрос
Здесь выполняется весь наш код, который мы пишем на JS. Первоначально все запросы, которые мы делаем, попадают именно сюда, и именно здесь Node.js может быть заблокирована. Если сюда попадёт какая-либо тяжёлая операция по вычислению, то на этом этапе наше приложение может просто зависнуть и ожидать, пока не выполнится данная операция.
На слайде 9 функция опроса содержится в строке 370: uv_io_poll(loop, timeout).
5 фаза — проверка
В Node.js есть таймер setImmediate, его callback’и выполняются на этой фазе.
В исходном коде это строка 371: uv_run_check(loop).
6 фаза (последняя) — callback’и событий close
Например, web-сокету нужно закрыть соединение, на этой фазе будет вызван callback этого события.
В исходном коде это строка 372: uv_run_closing_handless(loop).
И в итоге Event Loop Node.js выглядит следующим образом
Сначала в очереди таймеров выполняется тот таймер, срок которого подошёл.
Дальше выполняются I/O-callback’и.
Затем — основой код, дальше — setImmediate и события close.
После этого всё повторяется по кругу. Чтобы продемонстрировать это, открою код. Как он будет выполняться?
У нас нет таймеров в очереди, поэтому Event Loop двигается дальше. I/O-callback’ов тоже нет, поэтому идём сразу на фазу опроса. Весь код, который здесь есть, изначально выполняется на фазе опроса. Поэтому сначала печатаем script_start, setInterval у нас помещается в очередь таймеров (не выполняется, просто помещается). setTimeout также помещается в очередь таймеров, и затем выполняются промисы: сначала promise 1 и затем promise 2.
В следующий тик (цикл событий) мы возвращаемся на этап таймеров, здесь в очереди уже есть 2 таймера: setInterval и setTimeout. Они оба с задержкой 0, соответственно, они готовы к выполнению.
Выполняются (выводятся в консоль) setInterval, затем setTimeout 1. Callback’ов неблокирующего ввода/вывода нет, дальше будет фаза опроса, в консоль выводятся promise 3 и promise 4.
Дальше регистрируется таймер setTimeout. На этом тик заканчивается, идём в следующий тик. Там — снова таймеры, выполняется вывод в консоль setInterval и setTimeout 2, затем выводятся promise 5 и promise 6.
Event Loop мы рассмотрели и теперь можем более подробно поговорить о многопоточности.
Многопоточность — модуль worker_threads
Многопоточность появилась в Node. js благодаря модулю worker_threads в версии 10.5. И в 10-й версии она запускалась исключительно с ключом —experimental-worker, а с 11-й версии стал возможным запуск без него.
Ещё в Node.js есть модуль cluster, но он не поднимает потоки — он поднимает ещё несколько процессов. Масштабируемость приложения — его основная цель.
Как вообще выглядит 1 процесс:
1 процесс Node.js, 1 поток, 1 Event Loop, 1 движок V8 и libuv.
Если мы запускаем X потоков, то у нас это выглядит так:
1 процесс Node.js, X потоков, Х Event Loop’ов, X движков V8 и X libuv.
Схематично это выглядит следующим образом
Давайте разберём пример.
Простейший web-сервер на Express’е. Есть 2 route’а — / и /fat-operation.
Также есть функция generateRandomArr(). Она наполняет массив двумя миллионами записей и сортирует его. Запустим сервер.
Делаем запрос на /fat-operation. И в тот момент, когда выполняется операция сортировки массива, отправляем ещё один запрос на route /, но для получения ответа нам приходится ждать до тех пор, пока не выполнится сортировка массива. Это классическая реализация через один поток. Теперь подключаем модуль worker_threads.
Делаем запрос на /fat-operation и следом — на /, от которого тут же получаем ответ — Hello world!
Для операции сортировки массива мы подняли отдельный поток, у которого есть свой экземпляр Event Loop, и он никак не влияет на выполнение кода в основном потоке.
Поток будет «уничтожен», когда у него не будет операций для выполнения.
Смотрим исходный код. Регистрируем worker в строке 26 и, если нужно, передаём ему данные. В данном случае я ничего не передаю. И затем подписываемся на события: на ошибку и на месседж. В самом worker’е происходит вызов функции, массив из двух миллионов записей сортируется. Как только он отсортировался, мы через post_message отправляем результат в основной поток ok.
В основном потоке мы ловим это сообщение и отправляем результат finish. У worker’а и основного потока общая память, поэтому мы имеем доступ к глобальным переменным всего процесса. Когда мы передаём данные из основного потока в worker, worker получает только копию.
Основной поток и поток worker’а мы можем описать в одном файле. Модуль worker_threads предоставляет API, благодаря которому мы можем определить, в каком потоке сейчас выполняется код.
Дополнительная информация
Делюсь ссылками на полезные ресурсы и ссылкой на презентацию Райана Дала, когда он презентовал Event Loop (интересно посмотреть).
Event Loop
- Перевод статьи из документации Node.js
- https://blog.risingstack.com/node-js-at-scale-understanding-node-js-event-loop/
- https://habr.com/ru/post/336498/
Worker_threads
- https://nodejs.org/api/worker_threads.html#worker_threads_worker_workerdata — API
- https://habr.com/ru/company/ruvds/blog/415659/
- https://nodesource.com/blog/worker-threads-nodejs/
- Original slides from Ryan Dahl’s presentation (through VPN)
Многопоточность Nodejs против однопоточности nodejs
Я не понимаю разницы в многопоточной системе java и системе NodeJS mutltithread с точки зрения производительности и совместного использования ресурсов. Как NodeJS используйте цикл событий один поток для вашей программы, но за сценой он назначает задачу различным потокам, таким как чтение файлов или запросы к БД. таким образом, он использует многопоточность и threadpool (аналогично Java?).
Но всякий раз, когда мы сравниваем производительность, приложения NodeJS намного лучше, чем другие многопоточные системы.
Как на самом деле NodeJS обрабатывать многопоточные задачи программирования, такие как переполнение или блокировка потока. Как он разделяет ресурсы между потоками, например, я обращаюсь к одному и тому же файлу одновременно с двумя вводами-выводами, поэтому будет два потока, обращающихся к одному ресурсу, применяется ли он в многопоточной системе NodeJS? Или я неправильно понял этот момент?
node.js
multithreading
Поделиться
Источник
Shad
15 февраля 2016 в 08:33
2 ответа
- буферы nodejs против типизированных массивов
Что более эффективно-буферы nodejs или типизированные массивы? Что я должен использовать для повышения производительности? Я думаю, что только те, кто знает интерьеры V8 и NodeJs, могли бы ответить на этот вопрос.
- Библиотека Nodejs без nodejs
Как я могу интегрировать библиотеку nodejs в мой проект nodejs? Мне особенно нужна эта библиотека: https://github.com/greenify/biojs-io-blast
2
Nodejs использует libuv для этих целей, который написан на C.
Вот почему вы не можете сравнить Java и Nodejs, мы можем сказать, что Nodejs использует механизм низкого уровня для создания асинхронного IO.
libuv предназначен для nodejs, но его можно использовать в любых проектах.
Вы упомянули асинхронные операции с дисками — вы можете найти хороший пост об этом здесь .
Краткая версия:
используйте асинхронный диск I/O, вместо синхронных вызовов диска в потоке диска в версиях 0.16.x.
Что это значит? Это означает, что вы можете использовать тот же подход(асинхронные низкоуровневые операции IO), и я уверен, что вы можете поднять ту же скорость, например, с Java.
Другая вещь, которую вы упомянули-цикл событий. Нет ничего сложного — это легко понять, например вы можете прочитать этот хороший пост.
Поделиться
alexey
11 апреля 2016 в 09:09
2
вот мои 2 пенса стоит …
Многопоточность возможность
Правда: Node.js ( в настоящее время ) не обеспечивает встроенную поддержку многопоточной обработки в смысле низкоуровневого выполнения/обработки потоков. Java, и его реализации /frameworks, обеспечивает встроенную поддержку многопоточности, и широко тоже ( упреждение, мульти-аренда, синхронная многопоточность, многозадачность, пулы потоков и т. д )
Брюки в огне (ish): отсутствие многопоточности в Nodejs-это шоу-пробка. Nodejs построен вокруг архитектуры, управляемой событиями, где события создаются и потребляются как можно быстрее. Существует встроенная поддержка функциональных обратных вызовов. В зависимости от дизайна приложения, эта высокоуровневая функциональность может поддерживать то, что в противном случае могло бы быть сделано потоком. с
Для приложений на стороне сервера , на уровне приложения, что важно, так это возможность выполнять несколько задач одновременно: т. е. многозадачность. Существует несколько способов реализации многозадачности . Многопоточность является одним из них и естественным образом подходит для этой задачи. Тем не менее, концепция “многопоточности “ — это аспект платформы низкого уровня. Например, многопоточная платформа , такая как java, размещенная/работающая на одноядерном процессорном сервере (сервер с 1 CPU процессорным ядром), по-прежнему поддерживает многопоточность на уровне приложения, сопоставляемую с многопоточностью на низком уровне , но в действительности только один поток может выполняться на любом ontime. На многоядерной машине с 4 ядрами sa y поддерживается такая же многозадачность на уровне приложения, и до 4 потоков могут выполняться одновременно в любой момент времени. Дело в том, что в большинстве случаев действительно важна поддержка многозадачности, которая не всегда синонимична многопоточности.
Возвращаясь к node.js, настоящая дискуссия должна быть посвящена дизайну и архитектуре приложений, а более конкретно-поддержке многозадачности. В общем, существует целый сдвиг парадигмы между приложениями на стороне сервера и клиентскими или автономными приложениями, особенно с точки зрения дизайна и потока процессов. Среди прочего, серверные приложения должны работать вместе с другими приложениями( на сервере ), должны быть устойчивыми и самодостаточными (не влиять на работу сервера при сбое или сбое приложения ) , выполнять надежную обработку исключений ( т. е. восстанавливаться после ошибок, даже критических ) и выполнять несколько задач .
- Просто возможность поддержки многозадачности является критической возможностью для любой технологии на стороне сервера . И node.js имеет эту возможность, и представлен в очень простой в использовании упаковке . Это все означает, что дизайн для приложений на стороне sever должен больше фокусироваться на многозадачности, а не только на многопоточности. Да, конечно, работа на серверной платформе, поддерживающей многопоточность, имеет свои очевидные преимущества (расширенные функциональные возможности , производительность), но это само по себе не устраняет необходимость поддержки многозадачности на уровне приложения . Любой твердый дизайн приложения для серверных приложений, AND node.js должен основываться на многозадачности через генерацию и потребление событий (обработка событий). В node.js ключевое значение имеет использование обратных вызовов функций и небольших процессоров событий (как функций) с контрольной точкой данных ( сохранение данных обработки в файлах или базах данных) в экземплярах обработки событий.
Поделиться
young chisango
10 декабря 2017 в 13:49
Похожие вопросы:
REST API: nodejs против python
Я хочу сделать Restful API для текущего приложения PHP с базой данных mongodb backend. NodeJS (экспресс) против Python (web.py), что лучше для производительности?
Токены Gemalto OTP с NodeJS
Существуют ли какие-либо модули NodeJS, которые позволят мне аутентифицироваться против токенов Gemalto IDProve 100 OTP? У меня есть один из них от Amazon, и я думал, что красота этого устройства…
Atlassian Bamboo CI + NodeJS
Я использую Atlassian Bamboo для своих проектов PHP, и я нахожу его чрезвычайно полезным, NodeJS существует уже некоторое время, и я действительно удивлен, что Bamboo не поддерживает NodeJS из…
буферы nodejs против типизированных массивов
Что более эффективно-буферы nodejs или типизированные массивы? Что я должен использовать для повышения производительности? Я думаю, что только те, кто знает интерьеры V8 и NodeJs, могли бы ответить…
Библиотека Nodejs без nodejs
Как я могу интегрировать библиотеку nodejs в мой проект nodejs? Мне особенно нужна эта библиотека: https://github. com/greenify/biojs-io-blast
Модульный шаблон против прототипа-Nodejs?
Я работаю в Nodejs . Я работал в modular pattern году . Это легко и просто кодировать. Примечание Моя коллега рассказывала, шаблон прототипа является наилучшим подходом для Nodejs и модульные…
Nodejs C/C++ будет использовать более одного ядра?
Я точно знаю , что NodeJS работает в одном ядре. Но что произойдет, если я создам аддон NodeJS C/C++, в котором реализована многопоточность? Этот аддон будет потреблять более одного ядра (когда это…
Многопоточность nodeJS и сокет IO
Итак, многопоточность nodeJS не является большой проблемой из того, что я читал. Просто разверните несколько идентичных приложений и используйте nginx в качестве обратного прокси-сервера и…
Использует ли сервер NodeJS многопоточность?
У меня есть вопрос о nodeJS (особенно в отношении версии 9). Используя этот проект- https://github. com/howardchung/jsminer , я запускаю сервер nodeJS, если это правильное слово. Я раскручиваю его по…
В чем разница между многопоточностью и асинхронностью в NodeJS
NodeJs добавить функцию многопоточности в последнем обновлении. Я хотел бы понять на простых примерах, в чем разница между многопоточностью и асинхронностью ? В каких случаях мы должны использовать…
node.js — Многопоточность Nodejs против однопоточной nodejs
Я не понимаю разницы между многопоточной системой Java и многопоточной системой Nodejs с точки зрения производительности и совместного использования ресурсов. Поскольку NodeJS использует одиночный поток цикла событий для вашей программы, но за сценой он назначает задачу различным потокам, таким как чтение файлов или запросы к базе данных. так что там он использует многопоточность и пул потоков (аналогично Java?). Но всякий раз, когда мы сравниваем производительность, приложения NodeJS намного лучше, чем другие многопоточные системы.
Как на самом деле NodeJS справляется с проблемами многопоточного программирования, такими как переполнение или блокировка потока. Как он распределяет ресурсы между потоками, например, я обращаюсь к одному и тому же файлу одновременно с двумя операциями ввода-вывода, поэтому два потока будут обращаться к одному ресурсу, применяется ли это в многопоточной системе NodeJS? Или я неправильно понял этот момент?
2
Shad
15 Фев 2016 в 11:33
2 ответа
Лучший ответ
Nodejs использует для этих целей libuv, написанную на C.
Вот почему нельзя сравнивать Java и Nodejs, можно сказать, что Nodejs использует низкоуровневый механизм для выполнения асинхронного ввода-вывода.
Libuv разработан для nodejs, но его можно использовать в любых проектах.
Вы упомянули операции с асинхронным диском — вы можете найти хорошую запись об этом здесь.
Укороченная версия:
использовать асинхронный дисковый ввод-вывод вместо синхронных дисковых вызовов в дисковом потоке в версиях 0.16.x.
Что это значит? Это означает, что вы можете использовать тот же подход (асинхронные низкоуровневые операции ввода-вывода), и я уверен, что вы можете повысить ту же скорость, например, с Java.
Еще одна вещь, о которой вы упомянули, — цикл событий. Нет ничего сложного — это легко понять, например вы можете прочтите это хорошее Почта.
2
alexey
11 Апр 2016 в 09:09
Вот мои 2 пенса стоят …
Многопоточность
Правда: Node.js (в настоящее время) не предоставляет встроенную поддержку многопоточности в смысле низкоуровневых потоков выполнения / обработки. Java и ее реализации / инфраструктуры предоставляют встроенную поддержку многопоточности, а также экстенсивно (упреждающее, многопользовательское, синхронное многопоточность, многозадачность, пулы потоков и т. Д.)
Pants on Fire (ish): отсутствие многопоточности в Nodejs — ограничитель шоу. Nodejs построен на основе событийно-ориентированной архитектуры, где события создаются и потребляются как можно быстрее. Существует встроенная поддержка функциональных обратных вызовов. В зависимости от дизайна приложения, эта функциональность высокого уровня может поддерживать то, что иначе могло бы быть сделано потоком. s
Для серверных приложений на уровне приложений важна способность одновременно выполнять несколько задач, то есть многозадачность. Существует несколько способов реализации многозадачности. Многопоточность является одним из них и естественным образом подходит для этой задачи. Тем не менее, концепция «многопоточности» является аспектом платформы низкого уровня. Например, многопоточная платформа, такая как java, размещенная / работающая на одноядерном сервере процессов (сервер с 1 процессорным ядром), по-прежнему поддерживает multi-multi на уровне приложений, сопоставленную с многопоточностью на низком уровне, но в действительности только один поток может выполняться в любое время. На многоядерном компьютере с четырьмя ядрами поддерживается одна и та же многозадачность на уровне приложений, и до 4 потоков могут выполняться одновременно в любой момент времени. Дело в том, что в большинстве случаев действительно важна поддержка многозадачности, которая не всегда является синонимом многопоточности.
Возвращаясь к node.js, следует обсудить дизайн и архитектуру приложений и, в частности, поддержку MULTI-TASKING. В целом, между серверными приложениями и клиентскими или автономными приложениями существует целая смена парадигмы, в особенности с точки зрения дизайна и потока процессов. Помимо прочего, серверные приложения должны запускаться параллельно с другими приложениями (на сервере), должны быть устойчивыми и автономными (не затрагивать остальную часть сервера в случае сбоя или сбоя приложения), выполнять надежную обработку исключений (т.е. восстанавливаться после ошибки, даже критические) и должны выполнять несколько задач.
- Способность поддерживать многозадачность является критически важной для любой серверной технологии. И у node.js есть такая возможность, и она представлена в очень простой в использовании упаковке. Все это означает, что при разработке приложений для нескольких сторон необходимо уделять больше внимания многозадачности, а не просто многопоточности. Да, работа на серверной платформе, которая поддерживает многопоточность, имеет свои очевидные преимущества (расширенные функциональные возможности, производительность), но сама по себе она не устраняет необходимость поддержки многозадачности на уровне приложений. Любой надежный дизайн приложения для серверных приложений, а также node.js должен основываться на многозадачности посредством генерации и потребления событий (обработки событий). В node.js ключевым является использование обратных вызовов функций и небольших обработчиков событий (в качестве функций) с контрольными точками данных (сохранение данных обработки в файлах или базах данных) в экземплярах обработки событий.
2
young chisango
10 Дек 2017 в 13:49
35404648
Каковы недостатки создания многопоточной реализации JavaScript во время выполнения? [закрыто]
1) Многопоточность чрезвычайно сложна, и, к сожалению, то, как вы представили эту идею, подразумевает, что вы сильно недооцениваете ее сложность.
На данный момент кажется, что вы просто «добавляете темы» к языку и беспокоитесь о том, как сделать его правильным и производительным позже. В частности:
если две задачи пытаются получить доступ к переменной одновременно, она помечается как атомарная, и они борются за доступ.
…
Я согласен, что атомарные переменные не решат все, но моей следующей целью является работа над решением проблемы синхронизации.
Добавление потоков в Javascript без «решения проблемы синхронизации» было бы похоже на добавление целых чисел в Javascript без «решения проблемы добавления». Это настолько фундаментально для природы проблемы, что нет смысла даже обсуждать, стоит ли добавлять многопоточность без конкретного решения, независимо от того, насколько сильно мы этого хотим.
Кроме того, превращение всех переменных в атомарные — это то, что может заставить многопоточные программы работать хуже, чем их однопоточные аналоги, что делает еще более важным на самом деле тестировать производительность на более реалистичных программах и посмотреть, получаете ли вы что-нибудь или нет.
Мне также не ясно, пытаетесь ли вы скрыть потоки от программиста node.js или планируете в какой-то момент разоблачить их, эффективно создавая новый диалект Javascript для многопоточного программирования. Оба варианта потенциально интересны, но, похоже, вы еще не определились, к какому из них вы стремитесь.
Итак, на данный момент вы просите программистов подумать о переходе от однопоточной среды к совершенно новой многопоточной среде, у которой нет решения проблемы синхронизации, нет доказательств того, что она повышает реальную производительность, и, по-видимому, нет плана решения этих проблем.
Наверное, поэтому люди не воспринимают тебя всерьез.
2) Простота и надежность единого цикла обработки событий является
огромным преимуществом.
Программисты Javascript знают, что язык Javascript «безопасен» от условий гонки и других чрезвычайно коварных ошибок, которые мешают всему действительно многопоточному программированию. Тот факт, что им нужны веские аргументы, чтобы убедить их отказаться от того, что безопасность не делает их замкнутыми, делает их ответственными.
Если вы не можете каким-то образом сохранить эту безопасность, любому, кто захочет переключиться на многопоточный node.js, вероятно, было бы лучше перейти на язык наподобие Go, который изначально разрабатывался для многопоточных приложений.
3) Javascript уже поддерживает «фоновые потоки» (WebWorkers) и асинхронное программирование без непосредственного предоставления управления потоками программисту.
Эти функции уже решают многие распространенные случаи использования, которые затрагивают программистов Javascript в реальном мире, не отказываясь от безопасности одного цикла обработки событий.
Есть ли у вас какие-то конкретные примеры использования, которые эти функции не решают, и для которых программисты Javascript хотят найти решение? Если это так, было бы неплохо представить ваш многопоточный файл node.js в контексте этого конкретного варианта использования.
PS Что бы убедило
меня попробовать перейти на многопоточную реализацию node. js?
Напишите в Javascript / node.js нетривиальную программу, которая, по вашему мнению, выиграет от подлинной многопоточности. Выполните тесты производительности в этом примере программы на обычном узле и многопоточном узле. Покажите мне, что ваша версия в значительной степени повышает производительность, скорость отклика и использование нескольких ядер без каких-либо ошибок или нестабильности.
Как только вы это сделаете, я думаю, вы увидите, что люди гораздо больше заинтересованы в этой идее.
Понимание Event Loop в NodeJS
1- Обзор про NodeJS Event Loop
NodeJS это однопоточное приложение (Single Thread), работает на платформе написанной с помощью C++, данная платформа использует многопоточность (Multi-Thread) для одновременного выполнения заданий.
Изображение ниже иллюстрирует запросы (request) со стороные пользователя, отправленные к серверу NodeJS.
Каждый запрос (request) со стороны пользователя для NodeJS является событием (event), они расположены в Event Queue (Очередь событий). NodeJS испольует принцип FIFO (First In First Out — Первым прибыл, первым обслужен), это значит запросы пришедшие первыми будут обработаны первыми.
Event Loop
Как бесконечный цикл, он передает запросы в Thread Pool (Пул потоков), в то же время каждый запрос будет зарегистрирован функцией Callback. При завершении обработки запроса, соотвествующая функция Callback будет вызвана для выполнения.
Thread Pool
Как программа написанная языком C++, она поддерживает многопоточность (Multi Threads), поэтому здесь запросы будут обработаны на разных потоках. NodeJS так же поддерживает мультипроцессы (Multi Processes), это значит они могут быть выполнены на разных ядрах (Core).
После того, как обработан запрос, NodeJS вызовет функцию Callback (Зарегистрирован ддля данного запроса) чтобы выполнить его.
ЗАКЛЮЧЕНИЯ:
Первое базовое заключение: Если каждое подключение к Server открывает поток (Thread), то тратится много объема памяти. Это доказано когда вы сравниваете Apache и Nginx (Два Web Server развертывающие приложения PHP). Apache использовало намного больше памяти, по сравнению с Nginx.
NodeJS похож на Nginx тем, что использует один поток (Single thread) для получения подключений от пользователей, и считает каждый запрос пользователя событием.
Второе базовое заключение: Операции I/O (ввода — вывода) очень ресурсоемкие для системы, поэтому NodeJS строго контролирует использование операций I/O. Поэтому вам нужно использовать только Callback когда вы выполняете задания связанные с I/O.
В основном, очень много вещей в NodeJS работают параллельно на разных потоках, но они контролируются с помощью NodeJS, например Thread Pool. То что вы написали работает на одном потоке (single thread).
Как устроена Node.js. Event Loop и многопоточность | DevBlog kt.team
Владимир Зейналов, разработчик kt.team
Инфа будет полезна JS-разработчикам, которые хотят глубоко понимать суть работы с Node.js и Event Loop. Ты сможешь осознанно и более гибко управлять потоком выполнения программы (web-сервера).
Эту статью я составил по материалам своего недавнего доклада для коллег.
В конце статьи есть полезные материалы для самостоятельного изучения.
Как устроена Node.js. Возможности асинхрона
Давай посмотрим на этот код: он отлично демонстрирует синхронность выполнения кода в Node.js. Делается запрос куда-то на GitHub, затем читается файл и выводится результат в консоли. Что понятно из этого синхронного кода?
Слайд № 1
Предположим, что это абстрактный web-сервер, который по какому-то роутеру делает операции. Если по данному роутеру приходит входящий запрос, мы делаем дальше запрос, читаем файл, выводим в консоль. Соответственно, то время, которое тратится на запрос и чтение файла, сервер будет заблокирован, никакие другие входящие запросы он обрабатывать не сможет, выполнять другие операции — тоже.
Какие есть варианты решения данной проблемы?
- Многопоточность
- Неблокирующий ввод/вывод
Для первого варианта (многопоточность) есть хороший пример с web-сервером Apache vs Nginx.
Слайд № 2
Раньше Apache под каждый входящий запрос поднимал поток: сколько было запросов, столько же было и потоков. В это время Nginx имел преимущество, т. к. использовал неблокирующий ввод/вывод. Здесь ты можешь видеть, что с увеличением количества входящих запросов увеличивается объём потребляемой памяти именно у Apache, а на следующем слайде количество обрабатываемых запросов в секунду с количеством подключений у Nginx выше.
Слайд № 3
Наглядно показано, что неблокирующий ввод/вывод эффективнее.
Неблокирующий ввод/вывод стал возможным благодаря современным операционным системам, которые предоставляют данный механизм — демультиплексор событий.
Демультиплексор — это механизм, который принимает от приложения запрос, регистрирует его и выполняет.
Слайд № 4
В верхней части схемы видно, что у нас есть приложение и в нём выполняются операции (пусть это будет чтение файла). Для этого делается запрос в демультиплексор событий, сюда отправляется ресурс (ссылка на файл), нужная операция и callback. Демультиплексор событий регистрирует этот запрос и возвращает управление непосредственно приложению — таким образом, оно не блокируется. Затем он выполняет операции над файлом, и после этого, когда файл будет прочитан, callback регистрируется в очереди на выполнение. Затем Event Loop постепенно синхронно обрабатывает каждый callback из этой очереди. И, соответственно, возвращает результат приложению. Дальше (если необходимо) всё делается снова.
Таким образом, благодаря данному неблокирующему вводу/выводу Node.js может быть асинхронным.
Уточню, что в данном случае неблокирующий ввод/вывод предоставляет нам именно операционная система. К неблокирующему вводу/выводу (вообще в принципе к операциям ввода/вывода) мы относим сетевые запросы и работу с файлами.
Это общая концепция неблокирующего ввода/вывода. Когда такая возможность появилась, Райан Дал (Ryan Dahl) — разработчик Node.js — был вдохновлён опытом Nginx, которая использовала неблокирующий ввод/вывод, и решил создать платформу именно для разработчиков. Первое, что ему нужно было сделать, — «подружить» свою платформу с демультиплексором событий. Проблема была в том, что в каждой операционной системе демультиплексор реализован по-разному, и ему пришлось написать обёртку, которая впоследствии стала называться libuv. Это библиотека, написанная на C. Она предоставляет единый интерфейс работы с этими демультиплексорами событий.
Особенности libuv-библиотеки
Слайд № 5
В Linux, в принципе, на текущий момент все операции с локальными файлами — блокирующие. Т. е. вроде бы как есть неблокирующий ввод/вывод, но именно при работе с локальными файлами операция всё равно блокирующая. Именно поэтому для эмуляции неблокирующего ввода/вывода libuv использует внутри потоки. Из коробки поднимаются четыре потока, и здесь нужно сделать самый важный вывод: если мы выполняем какие-то четыре тяжёлые операции над локальными файлами, соответственно, мы заблокируем всё наше приложение (именно в ОС Linux, в других ОС такого нет).
Слайд № 6
На этом слайде мы видим архитектуру Node.js. Для взаимодействия с операционной системой используется библиотека libuv, написанная на C; для компиляции кода JavaScript’a в машинный код используется движок Google V8, также есть Node.js Core library, где собраны модули для работы с сетевыми запросами, файловой системой и модуль для логирования. Чтобы всё это друг с другом взаимодействовало, написаны Node. js Bindings. Эти четыре компонента составляют саму структуру Node.js. Сам механизм Event Loop’a находится в libuv.
Event Loop
Слайд № 7
Это простейшее представление, как выглядит Event Loop. Есть определённая очередь событий, есть бесконечный цикл событий, который синхронно выполняет операции из очереди, и он распределяет их дальше.
На этом слайде показано, как выглядит Event Loop непосредственно в самой Node.js.
Слайд № 8
Там реализация поинтереснее и посложнее. По сути, Event Loop — это цикл событий, и он бесконечен до тех пор, пока есть что выполнять. Event Loop в Node.js делится на несколько фаз (фазы со слайда 8 надо сопоставлять с исходным кодом на слайде 9).
Слайд № 9
Первая фаза — таймеры.
Данная фаза выполняется непосредственно Event Loop’ом. Обрати внимание на фрагмент кода с uv_update_time на слайде 9 — здесь просто обновляется время, когда начал работать Event Loop.
uv_run_timers — в этом методе выполняется следующее действие с таймером. Есть определённый стек, точнее, куча таймеров, — это, по сути, то же самое, что очередь, где находятся таймеры. Берётся таймер с самым маленьким временем, сравнивается с текущим временем Event Loop’а, и, если настало время для исполнения данного таймера, выполняется его callback. Здесь стоит отметить, что в Node.js есть реализация setTimeout и есть setInterval. Для libuv это, по сути, одно и то же, только в setInterval ещё есть флаг repeat.
Соответственно, если у данного таймера стоит флаг repeat, то он снова помещается в очередь событий и потом точно так же обрабатывается.
Вторая фаза — I/O-callback’и.
Здесь надо вернуться к диаграмме про неблокирующий ввод/вывод.
Когда демультиплексор событий выполняет чтение какого-либо файла и ставит выполнение callback’а в очередь, это как раз соответствует этапу I/O-callback. Здесь выполняются callback’и для неблокирующего ввода/вывода, т. е. это именно те функции, которые используются после запроса в базу данных или другой ресурс или на чтение/запись файла. Они выполняются именно на данной фазе.
На слайде 9 выполнение функции I/O-callback’и запускает строка 367: ran_pending = uv_run_pending(loop).
Третья фаза — ожидание, подготовка.
Это внутренние операции для callback’ов, по сути, мы не можем влиять на фазу, только косвенно. Есть process.nextTick, его callback может ненамеренно быть исполнен на фазе «ожидание, подготовка». process.nextTick выполняется на текущей фазе, т. е., по сути, process.nextTick может сработать абсолютно на любой фазе. Какого-то готового инструмента, чтобы запустить код на фазе «ожидание, подготовка», в Node.js нет.
На слайде 9 этой фазе соответствуют строки 368, 369:
uv_run_idle(loop) — ожидание;
uv_run_prepare(loop) — подготовка.
Четвёртая фаза — опрос.
Здесь выполняется весь наш код, который мы пишем на JS. Первоначально все запросы, которые мы делаем, попадают именно сюда, и именно здесь Node.js может быть заблокирована. Если сюда попадёт какая-либо тяжёлая операция по вычислению, то на этом этапе наше приложение может просто зависнуть и ожидать, пока не выполнится данная операция.
На слайде 9 функция опроса содержится в строке 370:
uv_io_poll(loop, timeout).
Пятая фаза — проверка.
В Node.js есть таймер setImmediate, его callback’и выполняются на этой фазе.
В исходном коде это строка 371:
uv_run_check(loop).
Шестая фаза (последняя) — callback’и событий close.
Например, web-сокету нужно закрыть соединение, на этой фазе будет вызван callback этого события.
В исходном коде это строка 372:
uv_run_closing_handless(loop).
И в итоге Event Loop Node.js выглядит следующим образом.
Слайд № 10
Сначала в очереди таймеров выполняется тот таймер, срок которого подошёл.
Дальше выполняются I/O-callback’и.
Затем — основной код, дальше — setImmediate и события close.
После этого всё повторяется по кругу. Чтобы продемонстрировать это, открою код. Как он будет выполняться?
У нас нет таймеров в очереди, поэтому Event Loop двигается дальше. I/O-callback’ов тоже нет, поэтому идём сразу на фазу опроса. Весь код, который здесь есть, изначально выполняется на фазе опроса. Поэтому сначала печатаем script_start, setInterval у нас помещается в очередь таймеров (не выполняется, просто помещается). setTimeout также помещается в очередь таймеров, и затем выполняются промисы: сначала promise 1 и затем promise 2.
В следующий тик (цикл событий) мы возвращаемся на этап таймеров, здесь в очереди уже есть два таймера: setInterval и setTimeout. Оба с нулевой задержкой, соответственно, они готовы к выполнению.
Выполняются (выводятся в консоль) setInterval, затем setTimeout 1. Callback’ов неблокирующего ввода/вывода нет, дальше будет фаза опроса, в консоль выводятся promise 3 и promise 4.
Дальше регистрируется таймер setTimeout. На этом тик заканчивается, идём в следующий тик. Там — снова таймеры, выполняется вывод в консоль setInterval и setTimeout 2, затем выводятся promise 5 и promise 6.
Event Loop мы рассмотрели и теперь можем более подробно поговорить о многопоточности.
Многопоточность — модуль worker_threads
Многопоточность появилась в Node.js благодаря модулю worker_threads в версии 10.5. И в 10-й версии она запускалась исключительно с ключом —experimental-worker, а с 11-й версии стал возможным запуск без него.
Ещё в Node.js есть модуль cluster, но он не поднимает потоки — он поднимает ещё несколько процессов. Масштабируемость приложения — его основная цель.
Как вообще выглядит один процесс:
один процесс Node.js, один поток, один Event Loop, один движок V8 и libuv.
Если мы запускаем X потоков, то у нас это выглядит так:
один процесс Node.js, X потоков, Х Event Loop’ов, X движков V8 и X libuv.
Схематично это выглядит следующим образом.
Слайд № 11
Давай разберём пример.
Простейший web-сервер на Express’е. Есть 2 route’а — / и /fat-operation.
Также есть функция generateRandomArr(). Она наполняет массив двумя миллионами записей и сортирует его. Запустим сервер.
Делаем запрос на /fat-operation. И в тот момент, когда выполняется операция сортировки массива, отправляем ещё один запрос на route /, но для получения ответа нам приходится ждать до тех пор, пока не выполнится сортировка массива. Это классическая реализация через один поток. Теперь подключаем модуль worker_threads.
Делаем запрос на /fat-operation и следом — на /, от которого тут же получаем ответ — Hello world!
Для операции сортировки массива мы подняли отдельный поток, у которого есть свой экземпляр Event Loop, и он никак не влияет на выполнение кода в основном потоке.
Поток будет «уничтожен», когда у него не будет операций для выполнения.
Смотрим исходный код. Регистрируем worker в строке 26 и, если нужно, передаём ему данные. В данном случае я ничего не передаю. И затем подписываемся на события: на ошибку и на месседж. В самом worker’е происходит вызов функции, массив из двух миллионов записей сортируется. Как только он отсортировался, мы через post_message отправляем результат в основной поток ok.
В основном потоке мы ловим это сообщение и отправляем результат finish. У worker’а и основного потока общая память, поэтому мы имеем доступ к глобальным переменным всего процесса. Когда мы передаём данные из основного потока в worker, worker получает только копию.
Основной поток и поток worker’а мы можем описать в одном файле. Модуль worker_threads предоставляет API, благодаря которому мы можем определить, в каком потоке сейчас выполняется код.
Дополнительная информация
Делюсь ссылками на полезные ресурсы и ссылкой на презентацию Райана Дала, когда он презентовал Event Loop (интересно посмотреть).
Event Loop
- Перевод статьи из документации Node.js
- https://blog.risingstack.com/node-js-at-scale-understanding-node-js-event-loop/
- https://habr.com/ru/post/336498/
worker_threads
- https://nodejs.org/api/worker_threads.html#worker_threads_worker_workerdata — API
- https://habr. com/ru/company/ruvds/blog/415659/
- https://nodesource.com/blog/worker-threads-nodejs/
- Original slides from Ryan Dahl’s presentation (through VPN)
javascript — возможно ли добиться многопоточности в nodejs?
Да и нет. Начнем с начала. Почему NodeJs однопоточный, объясняется здесь Почему Node.js однопоточный?
Хотя сам Node.js является многопоточным — ввод-вывод и другие подобные операции выполняются из пула потоков, — код JavaScript, выполняемый Node.js, выполняется для всех практических целей в одном потоке. Это ограничение не для самого Node.js, а для движка JavaScript V8 и реализации JavaScript в целом.
Node.js включает собственный механизм для кластеризации нескольких процессов Node.js, где каждый процесс выполняется на отдельном ядре. Но этот механизм кластеризации не включает в себя никакой собственной логики маршрутизации или общего состояния между рабочими.
Обычно и более четко утверждается, что каждый процесс node. js является однопоточным. Если вам нужно несколько потоков, у вас также должно быть несколько процессов.
Например, вы можете использовать для этого дочерний процесс, который описан здесь http: // nodejs.org / api / child_process.html. И просто для вашей информации ознакомьтесь также с этой статьей, она очень поучительна и хорошо написана и, возможно, поможет вам, если вы хотите работать с child_processes — https://blog.scottfrees.com/automating-ac-program- из-узла-js-веб-приложение
Несмотря на все вышесказанное, вы можете добиться своего рода многопоточности с помощью C ++ и разработки собственных nodejs C ++.
Прежде всего ознакомьтесь с этими ответами, возможно они вам помогут,
Как создавать потоки в nodejs
Узел.js Аддон C ++: несколько обратных вызовов из разных потоков
Модуль C ++ для Node.js: многопоточность
Конечно, вы можете найти и использовать множество подключаемых модулей узлов, которые предоставляют возможность «многопоточности»: https://www. npmjs.com/search?q=thread
Дополнительно можно проверить JXCore https://github.com/jxcore/jxcore
JXCore является форком Node.js и позволяет Node.js для запуска в нескольких потоках, размещенных в одном процессе. Так что, скорее всего, JXCore — это решение для вас.
«Каковы преимущества и недостатки использования многопоточности в Node.js?»
Это зависит от того, что вы хотите делать. Нет никаких недостатков, если вы правильно используете и используете исходные коды Node.js, а ваши «многопоточные» плагины, процессы или что-то еще не «взламывают» и не используют неправильно что-либо из ядра V8 или Node.js!
Как и в любом другом ответе, правильный ответ — «используйте правильные инструменты для работы».Конечно, поскольку узел по своей конструкции является однопоточным, у вас могут быть лучшие подходы для многопоточности.
Техника, которую используют многие люди, заключается в создании своего многопоточного приложения на C ++, Java, Python и т. Д., А затем они запускают его с помощью автоматизации и Node.js child_process (стороннее приложение работает асинхронно с автоматизацией, у вас есть лучшая производительность (например, приложение C ++), и вы можете отправлять ввод и получать вывод в приложении Node.js и из него).
Недостатки многопоточности Node.js
Проверьте это: https://softwareengineering.stackexchange.com/questions/315454/what-are-the-drawbacks-of-making-a-multi-threaded-javascript-runtime-implementat
Имейте в виду, что если вы хотите создать чистую многопоточную среду в Node.js, изменив ее, я полагаю, что это будет сложно, рискованно из-за сложности, более того, вы должны быть всегда в курсе каждого нового V8 или Выпуск узла, который, вероятно, повлияет на это.
Надеюсь, это поможет.
Реализуйте многопоточность в Nodejs с реальным вариантом использования
В этой статье мы увидим формулировку проблемы и то, как вы можете решить ее, используя концепцию многопоточности nodejs. Реализуйте многопоточность в Nodejs с реальным вариантом использования.
Как мы все знаем, Nodejs — это однопоточная среда выполнения. Если вы новичок в концепции однопоточного узла. прочтите эту статью, чтобы лучше понять это.
Так как Nodejs является однопоточным.Основное ограничение, с которым столкнулись разработчики Nodejs, — это выполнение задач с интенсивным использованием ЦП в приложениях Node.
Но в Node.js v11 есть одна важная особенность. то есть рабочие потоки в Nodejs.
Что такое рабочие потоки
рабочих потоков в Nodejs решают проблему многопоточности. есть несколько сценариев, в которых вам может потребоваться запустить задачу в другом потоке, а не в основном потоке.
В этом случае рабочие потоки выполняют задачу, не блокируя основной поток приложения.
Если вы хотите узнать больше о базовой реализации рабочих потоков, обратитесь к этой статье, чтобы лучше понять основы.
Реализация — Постановка проблемы
Давайте посмотрим, где нам могут понадобиться рабочие потоки в Nodejs с вариантом использования. Допустим, вы создаете веб-приложение, в котором есть много стоковых видео.
Когда пользователь загружает видео, вы должны добавить водяной знак вашего логотипа приложения для защиты авторских прав.
давайте посмотрим, как можно реализовать этот сценарий с помощью рабочих потоков.
Добавление водяного знака к видео можно реализовать с помощью ffmpeg. но проблема с использованием этого в приложении nodejs в том, что оно требует интенсивного использования процессора.
Если вы напрямую реализуете это в своем приложении. он блокирует основной поток и влияет на производительность.
Решение
Итак, давайте реализуем это с помощью рабочих потоков.
Как всегда, создайте каталог и инициализируйте проект с помощью npm init —yes
Установите зависимости для проекта.
1npm i express body-parser ffmpeg hbs multer
- express — Nodejs Framework для обработки запроса и ответа
- body-parser — он обрабатывает тело сообщения запроса.
- ffmpeg — Эта библиотека используется для добавления водяных знаков к загруженному видео.
- hbs — handlebars — это шаблонизатор для визуализации представлений в экспресс-приложениях.
- multer — используется для обработки загрузки файлов в приложениях nodejs.
Здесь мы собираемся использовать babel для использования ES6 в приложении узла.babel в основном компилирует ES6 в javascript.
1npm i --save-dev @ babel / cli @ babel / core @ babel / node @ babel / preset-env dotenv nodemon
создайте файл с именем .babelrc и добавьте следующий код
1 {
2 «предустановки»: [
3 [
4 «@ babel / preset-env»,
5 {
6 «целей»: {
7 «узел»: true
8}
)9}
10]
11]
12}
После этого создайте каталог просмотров и внутри этого каталога создайте файл index.hbs и добавьте следующий код:
1
2
3
4
5 0" />
6
7
Водяной знак видео 8
9 rel = "stylesheet"
10 href = "https: // cdnjs.cloudflare.com/ajax/libs/semantic-ui/2.4.1/semantic.min.css "
11 />
12
13
14
15
Watermarkify
16
17 {{# if error}}
18
19
20
21 {{error}}
22
23
24 {{/ if}}
25
26
38
39
в основном у нас есть html fo rm, который принимает на вход видео файл.
После отправки формы нам нужно обработать маршрут, чтобы получить данные в экспрессе.
1импорт экспресс из "экспресса"
2импорт * как bodyParser из "body-parser"
3импорт * как путь из "path"
4импорт multer из "multer"
5
6require ("dotenv") .config ()
7
8const storage = multer.diskStorage ({
9 destination: "./uploads/",
10 filename: function (req, file, cb) {
11 cb (
) 12 null,
13 файлов.fieldname + "-" + Date.now () + path.extname (file.originalname)
14)
15},
16})
17
18const upload = multer ({dest: "uploads" , storage: storage})
19
20const app = express ()
21
22app.use (bodyParser.json ())
23app.use (bodyParser.urlencoded ({extended: false}))
24
25app.set ("views", path.join (__ dirname, "views"))
26app.set ("view engine", "hbs")
27
28app. get ("/", (req, res) => {
29 res.render ("index")
30})
31
32app.post ("/ upload-video", upload.single ( "ssvideo"), (req, res) => {
33 console.log (req.file)
34})
35
36const PORT = process.env.PORT
37
38app.listen (PORT, () => {
39 console.log (`Сервер работает на PORT $ {PORT}`)
40})
Здесь мы настраиваем механизм шаблонов руля вместе с multer в экспресс-приложении.
После этого у нас есть два маршрута, которые обрабатывают страницу индекса по умолчанию и загружают URL-адрес видео.
мы получим данные загруженного файла по адресу / upload-video , вы можете обработать видео в маршруте.
Теперь пора реализовать рабочие потоки.
Рабочие потоки — концепции
В рабочих потоках есть три основных понятия. это Worker , isMainThread и workerData .
В основном Worker используется для создания рабочего потока, а isMainThread проверяет, выполняется ли приложение в основном потоке, а рабочие данные используются для передачи данных из основного потока в рабочий поток.
В маршруте / upload-video проверьте, выполняется ли он в основном потоке, если это так, создайте рабочий поток и передайте данные загруженного файла рабочему потоку.
1app.post ("/ upload-video", upload.single ("ssvideo"), (req, res) => {
2 if (isMainThread) {
3 let thread = new Worker (". /threads/threaderone.js ", {
4 workerData: {
5 file: req.file.path,
6 filename: req.file.filename,
7 watermark_image_url: image_url,
8},
9})
10
11 резьб.on ("сообщение", data => {
12 res.download (data.file, req.file.filename)
13})
14
15 thread.on ("error", err => {
16 console.error ("thread", err)
17})
18
19 thread.on ("exit", code => {
20 if (code! = 0) console.error ( `Worker остановлен с кодом выхода $ {code}`)
21})
22}
23})
Здесь у нас есть три функции обратного вызова: сообщение, ошибка и выход. получать значения от разных событий.
Создание рабочих потоков
создайте файл с именем threadone.js внутри каталога thread и добавьте следующий код.
1let ffmpeg = require ("ffmpeg")
2const fs = require ("fs")
3const {workerData, parentPort} = require ("worker_threads")
4
5let dest = "/ dest / video.mp4 "
6
7 попыток {
8 let process = new ffmpeg (workerData.file)
9
10 process.затем (видео => {
11 video.fnAddWatermark (
12 __dirname + "/watermark.png",
13 __dirname + "/" + workerData.filename,
14 {
15 позиция: "C") ,
16},
17 функция (ошибка, файл) {
18 if (! Err) {
19 console.log ("Новый видеофайл -" + файл)
20
21 parentPort.postMessage ({status: "Done", file: file})
22}
23}
24)
25})
26} catch (e) {
27 console.log (e.code)
28 console. log (e.msg)
29}
По сути, он получает workerData из основного потока и выполняет весь процесс встраивания водяного знака в видео и отправляет сообщение в основной поток.
Главный поток получает измененное видео и отправляет его в качестве ответа пользователю.
В этом процессе основной поток не блокируется задачей, интенсивно использующей процессор.
Сводка
Полный исходный код приложения можно найти в репозитории
Реализация многопоточности в Nodejs с реальным сценарием использования.
Хотите выделиться из толпы?
Не зацикливайтесь на цикле обучения. Изучите React.js и Nodejs с помощью
практиковать сценарии реального мира и получить работу как босс.
Подпишитесь и получайте реальные сценарии проблем на свой почтовый ящик для
бесплатно
Подробнее
Node.js становится многопоточным: будущее Backend Framework JavaSript
Вы когда-нибудь думали об использовании Node.js для вычислений с интенсивным использованием ЦП? Если так, вы должны знать, что это не лучший способ. Однако это может измениться в будущем, поскольку на горизонте появляется новое лицо Node.js.
Node.js, однопоточная среда выполнения JavaScript, предлагает ряд преимуществ. Он обладает выдающейся производительностью неблокирующего ввода-вывода. Кроме того, он легкий и эффективный, а также обеспечивает беспрецедентную поддержку решений реального времени, таких как sockets.io или WebRTC. Он поставляется с самой большой экосистемой пакетов. Он идеально подходит для разработки микросервисной архитектуры. Все эти функции делают Node.js — отличная технология, но есть одна область, где Node.js терпит неудачу. Одиночный поток отлично подходит для обработки асинхронного кода, но определенно не оптимален для вычислений с интенсивным использованием ЦП. В Node.js любое трудоемкое вычисление блокирует цикл обработки событий, который, в свою очередь, блокирует все приложение.
Последние новости сообщества Node.js заставляют нас думать, что в ближайшем будущем ситуация изменится. В выпуске 10. 5.0 анонсирована многопоточность в Node.js. Эта функция все еще экспериментальная и, вероятно, претерпит значительные изменения, но она показывает направление, в котором Node.js находится в заголовке.
В последней версии представлен рабочий модуль, который предоставляет рабочие процессы, представляющие независимые потоки выполнения JavaScript. Рабочие позволяют запускать код в нескольких потоках, а также синхронизировать их с помощью каналов сообщений. Разработанные для обработки операций с интенсивным использованием ЦП, рабочие не предназначены для обработки асинхронного ввода-вывода. Более того, в отличие от кластерного режима или процессов, рабочие могут эффективно совместно использовать память. Если вы хотите углубиться в это, следуйте официальной документации.
Почему Node.js, хвастающийся своей однопоточностью, теперь реализовал функцию, которая полностью противоречит его фундаментальному предположению? Я считаю, что как только многопоточность станет полностью функциональной, приложения Node. js выиграют как от однопоточности, так и от многопоточности. Другими словами, Node.js останется однопоточным, если только не будет принято иное решение в соответствии с конкретными бизнес-требованиями. Наконец, Node.js предоставит место для графики, сложной обработки данных и машинного обучения.
Подробное изучение рабочих потоков в Node.js | автор: Deepal Jayasekara
В течение многих лет Node.js не был лучшим выбором для реализации приложений с высокой загрузкой процессора. Это главным образом потому, что Node.js — это просто Javascript, а JavaScript — однопоточный. В качестве решения проблемы Node.js v10.5.0 представил экспериментальную концепцию Worker Threads через модуль worker_threads
, который стал стабильной функциональностью, начиная с Node.js v12 LTS. В этой статье я подробно объясню, как они работают и как добиться максимальной производительности с помощью рабочих потоков.Если вы все еще не знакомы с рабочими потоками Node.js, я предлагаю вам ознакомиться с официальной документацией worker_threads
и сначала ознакомиться с ней 🙂.
Эта статья — вторая из моей серии Advanced NodeJS Internals Series, которая является продолжением моей серии статей о цикле событий Node.js. Вы можете найти другие статьи из серии Advanced NodeJS Internals ниже:
Дорожная карта серии сообщений
Заявление об ограничении ответственности! — Обратите внимание, что все фрагменты, на которые ссылается Node.js из ревизии 921493e.
До появления рабочих потоков существовало несколько способов выполнения приложений с интенсивным использованием ЦП с использованием Node.js. Некоторые из них:
- Использование модуля
child_process
и выполнение кода, интенсивно использующего процессор, в дочернем процессе. - Использование модуля кластера
- Использование стороннего модуля, такого как Napa. js.
Но ни одно из этих решений не получило широкого распространения из-за ограничений производительности, дополнительной сложности, недостаточного распространения, нестабильности или отсутствия документации.
Хотя worker_threads
представляет собой элегантное решение проблемы параллелизма JavaScript, оно не вводит языковые функции многопоточности в сам JavaScript. Вместо этого реализация worker_threads
обеспечивает параллелизм, позволяя приложениям использовать несколько изолированных рабочих процессов JavaScript, в которых связь между рабочими процессами и родительским рабочим процессом обеспечивается Node. Это сбивает с толку? 🤷♂️
I n Node.js, каждый воркер будет иметь свой собственный экземпляр V8 и Event Loop.Однако, в отличие от дочерних процессов, рабочие могут совместно использовать память.
Позже в этом посте я подробно объясню, как у них может быть собственный экземпляр V8 и цикл событий.
Прежде всего, давайте кратко рассмотрим, как мы можем использовать рабочие потоки. Наивный вариант использования рабочих потоков может выглядеть следующим образом. Назовем этот скрипт worker-simple.js
.
worker-simple.js
В приведенном выше примере мы передаем число отдельному исполнителю, чтобы вычислить его квадрат.После вычисления квадрата дочерний рабочий процесс отправляет результат обратно в основной рабочий поток. Хотя это звучит просто, это может показаться немного запутанным, если вы новичок в рабочих потоках Node.js.
Язык JavaScript не поддерживает многопоточность. Следовательно, рабочие потоки Node.js ведут себя иначе, чем традиционная многопоточность во многих других языках высокого уровня.
В Node.js работник отвечает за выполнение фрагмента кода (скрипта исполнителя), предоставленного родительским исполнителем.Затем рабочий сценарий будет работать изолированно от других рабочих процессов с возможностью передачи сообщений между ним и родительским рабочим. Рабочий скрипт может быть отдельным файлом или скриптом в текстовом формате, который может иметь вид eval
ed. В нашем примере мы предоставили __filename
в качестве рабочего скрипта, потому что оба родительских и дочерних кода рабочего процесса находятся в одном и том же скрипте, определяемом свойством isMainThread
.
Каждый рабочий подключается к своему родительскому рабочему через канал сообщений.Дочерний рабочий может писать в канал сообщений с помощью функции parentPort.postMessage
, а родительский рабочий может писать в канал сообщений, вызывая функцию worker.postMessage ()
для экземпляра рабочего. Взгляните на следующую диаграмму (Диаграмма 1).
Диаграмма 1: Канал сообщений между родителем и дочерними работниками
Канал сообщений — это простой канал связи. У него два конца, которые называются «портами». В терминологии JavaScript / NodeJS два конца канала сообщений называются просто «порт1» и «порт2».Как задумчиво! 😁
Как параллельно работают рабочие процессы Node.js?
Теперь возникает вопрос на миллион долларов: JavaScript не обеспечивает параллелизм сразу, как могут два воркера Node. js работать параллельно? Ответ: V8 изолирует .
Изолятор V8 — это независимый экземпляр среды выполнения chrome V8, который имеет собственную кучу JS и очередь микрозадач. Это позволяет каждому воркеру Node.js выполнять свой код JavaScript полностью изолированно от других воркеров.Обратной стороной этого является то, что рабочие не могут напрямую обращаться к кучам друг друга.
Из-за этого каждый рабочий будет иметь свою собственную копию цикла обработки событий libuv, которая не зависит от циклов событий другого рабочего и родительского рабочего.
Пересечение границы JS / C ++
Создание экземпляра нового исполнителя и обеспечение связи между родительским сценарием JS и сценарием рабочего JS устанавливается реализацией рабочего C ++. На момент написания этой статьи это реализовано в worker.cc.
Реализация Worker предоставляется сценариям JavaScript пользовательского уровня с использованием модуля worker_threads
. Эта реализация JS разделена на два скрипта, которые я хотел бы назвать как:
- Сценарий инициализации рабочего — отвечает за создание экземпляра рабочего экземпляра и настройку начального взаимодействия родитель-дочерний рабочий, чтобы разрешить передачу метаданных рабочего от родителя к детский работник.
- Worker Execution script — выполняет пользовательский рабочий JS-скрипт с предоставленными пользователем
workerData
и другими метаданными, предоставленными родительским worker.
Следующая диаграмма (диаграмма 2) поясняет это гораздо более ясно. Давайте рассмотрим то, что описано на этой диаграмме.
Диаграмма 2: Внутреннее устройство реализации работника
Основываясь на вышеизложенном, мы можем разделить процесс настройки рабочего на два этапа. Это:
- Инициализация воркера
- Запуск воркера
Давайте посмотрим, что происходит на каждом шаге.
Шаг инициализации
- Пользовательский сценарий создает рабочий экземпляр с помощью модуля
worker_threads
. - Сценарий инициализации родительского рабочего узла Node вызывает C ++ и создает экземпляр пустого рабочего объекта. На этом этапе созданный воркер — это не что иное, как простой объект C ++, который еще не запущен.
- Когда создается рабочий объект C ++, он генерирует идентификатор потока и назначает себя.
- Пустой канал сообщения инициализации создается (назовем его,
IMC
) родительским рабочим при создании рабочего объекта. Это показано на схеме 2 как «Канал сообщения инициализации». - Открытый канал сообщения JS создается сценарием инициализации рабочего (назовем его
PMC
).Это канал сообщений, который используется пользовательским JS для передачи сообщений между родительским и дочерним работниками с использованием функций* .postMessage ()
. Это кратко описано на схеме 1, а также может быть замечено красным цветом на схеме 2. - Сценарий инициализации родительского рабочего узла узла вызывает в C ++ и записывает метаданные инициализации в IMC, которые необходимо отправить сценарию выполнения рабочего. .
Что это за метаданные инициализации? Это данные, которые необходимо знать рабочему сценарию для запуска рабочего, включая имя сценария, который будет запускаться как рабочий, данные рабочего,
порт 2,
PMC и некоторую другую информацию.Согласно нашему примеру, метаданные инициализации — это просто сообщение типа:
Hey Worker Execution Script! Не могли бы вы запустить
worker-simple.js
с данными рабочего{num: 5}
? Также передайте емупорт 2
PMC, чтобы рабочий мог читать и писать в PMC.
Ниже приводится небольшой фрагмент, показывающий, как метаданные инициализации записываются в IMC.
Сценарий инициализации рабочего процесса записывает в IMC
В приведенном выше фрагменте, этот [kPort]
, является концом сценария инициализации IMC.Несмотря на то, что сценарий инициализации исполнителя записывает в IMC, сценарий выполнения рабочего не может получить доступ к этим данным, поскольку он еще не запущен.
Шаг выполнения
На этом инициализация завершена. Затем сценарий инициализации рабочего вызывает C ++ и запускает рабочий поток.
- Новый изолят v8 создается и назначается работнику. Изолят v8 — это независимый экземпляр среды выполнения v8. Это делает контекст выполнения рабочего потока изолированным от остального кода приложения.
- libuv инициализирован. Это позволяет рабочему потоку иметь собственный цикл обработки событий, независимый от остальной части приложения.
- Выполняется сценарий выполнения рабочего, и запускается цикл обработки событий рабочего.
- Сценарий выполнения работника вызывает C ++ и считывает метаданные инициализации из IMC.
- Сценарий выполнения Worker выполняет файл (или код), который должен быть запущен как worker. В нашем случае
worker-simple.js
.
См. Следующий отредактированный фрагмент о том, как сценарий выполнения рабочего процесса
Сценарий выполнения рабочего процесса читает из IMC
Вот замечательный вывод!
Вы заметили в приведенном выше фрагменте, что свойства workerData
и parentPort
установлены на объекте require ('worker_threads')
скриптом выполнения worker ??
Вот почему свойства workerData
и parentPort
доступны только внутри кода дочернего рабочего потока, но не в коде родительского рабочего потока.
Если вы попытаетесь получить доступ к этим свойствам в коде родительского рабочего, они оба вернут null
.
Теперь мы понимаем, как работают рабочие потоки Node.js. Понимание того, как они работают, действительно помогает нам добиться максимальной производительности при использовании рабочих потоков. При написании более сложных приложений, чем наш worker-simple.js
, мы должны помнить следующие две основные проблемы, связанные с рабочими потоками.
- Несмотря на то, что рабочие потоки легче, чем реальные процессы, создание рабочих потоков требует серьезной работы и может быть дорогостоящим, если выполняется часто.
- Использование рабочих потоков для распараллеливания операций ввода-вывода неэффективно, потому что использование собственных механизмов ввода-вывода Node.js намного быстрее, чем запуск рабочего потока с нуля только для этого.
Чтобы преодолеть первую проблему, нам нужно реализовать «пул рабочих потоков».
Пул рабочих потоков
Пул рабочих потоков Node.js — это группа запущенных рабочих потоков, которые могут использоваться для входящих задач. Когда поступает новая задача, ее можно передать доступному работнику через канал сообщений родитель-потомок.После того, как рабочий завершает задачу, он может передать результаты обратно родительскому рабочему через тот же канал сообщений.
При правильной реализации пул потоков может значительно улучшить производительность, поскольку снижает дополнительные накладные расходы на создание новых потоков. Также стоит упомянуть, что создание большого количества потоков также неэффективно, поскольку количество параллельных потоков, которые могут быть эффективно запущены, всегда ограничено оборудованием.
Следующий график представляет собой сравнение производительности трех узлов.js, которые принимают строку и возвращают хеш Bcrypt с 12 раундами соли. Это три разных сервера:
- Сервер без многопоточности
- Сервер с многопоточностью, но без какого-либо пула потоков
- Сервер с пулом потоков из 4 потоков
Как видно на первый взгляд, использование пула потоков имеет значительно меньшие затраты по мере увеличения рабочей нагрузки.
Рабочие потоки и многопоточность в Node.js
Моя жизнь казалась чередой событий и случайностей.Но когда я оглядываюсь назад, я вижу закономерность.
— Бенуа Мандельброт
Серия событий — так работает асинхронный цикл событий в Node.js. Из-за этого мы можем получить выгоду от неблокирующих операций ввода-вывода и параллельно ждать множества запланированных событий. Цикл событий позволяет Node.js обрабатывать множество операций длиной и в одном потоке. Этого достаточно для простого внутреннего сервера, но он работает, только если мы будем придерживаться правила: Не блокировать цикл событий .Как его можно заблокировать? Путем тяжелых вычислений, бесконечных циклов, синхронного ввода-вывода или других операций, которые заставляют двигатель работать. Вот место для другого решения: потоков .
Считается, что писать многопоточные программы сложно. Нам нужно позаботиться о разделяемой памяти, условиях гонки и многих других проблемах, которые могут вызвать непредсказуемую ошибку. Часто бывает, что эти ошибки нелегко обнаружить или повторить для отладки. Конечно, языки программирования предоставляют некоторые функции для предотвращения таких ситуаций.Обычно они предоставляют мьютексы, атомарные типы и операции, некоторые способы связи между потоками и т. Д. Но это усложняет эти языки.
Рабочие потоки
В этом посте я пишу о Node.js, который представляет собой среду JavaScript, а JavaScript всегда был прост. Так как же авторы Node справляются с потоками?
В 2018 году они выпустили Node v10.5.0 с экспериментальной функцией — рабочих потоков, , которая стала стабильной в январе 2019 года. Давайте посмотрим, как это работает!
Как я упоминал ранее, Node обычно работает в одном потоке.У нас есть один процесс, один поток, один фрагмент исполняемого кода, одна куча памяти, один цикл событий и один экземпляр узла. Когда мы используем воркеры, у нас остается один процесс, но много потоков. Для каждого потока существует отдельных : код, память, цикл событий и движок JS.
У можно иметь некоторую общую память с потоками Worker, используя, например, SharedArrayBuffer
. Но есть другой способ общения. Темы могут отправлять сообщения, используя портов . Когда рабочий поток отправляет сообщение с данными своему родительскому элементу, данные копируются, и копия становится доступной для родительского.И наоборот.
Рабочие потоки кажутся отличными. Мы избегаем проблем с условиями гонки и общей памятью, они просты в использовании (они просто выполняют код, который мы передаем, то есть имя файла своего скрипта) и в целом выглядят полезными. К сожалению, у них есть недостатки. Их дорогие . Для запуска нового Worker требуется некоторое время, потому что ему нужно запустить новый экземпляр Node. Копирование больших объемов данных между потоками также не кажется эффективным. Конечно, есть способы решения этих проблем.Один из них называется пулом потоков. Это шаблон проектирования, который используется, чтобы не запускать все время новые потоки, поддерживая постоянную активность некоторых потоков и ожидая выполнения задач. Я собираюсь показать вам простой пример решения, похожего на это.
Нарисуем!
Мне было интересно, что было бы хорошим примером использования потоков, и я подумал о построении набора Мандельброта. Он показан в начале сообщения, и этот конкретный сюжет был создан с использованием сценария, который я представляю ниже.
В нашем коде мы будем использовать две библиотеки — mathjs для работы с комплексными числами и canvas для рисования. Вы можете установить его с помощью этой команды:
npm установить холст mathjs
Чтобы понять, что мы собираемся получить, и измерить эффективность рабочих потоков, сначала мы напишем синхронную версию программы.
Это функция, которая вычисляет значения каждого пикселя на нашем графике.
const plotMandelbrot = (размер, границы) => {
const imgData = new Uint8ClampedArray (size.ш * размер. в * 4);
const samples = 4;
const maxIter = 256;
const color = {r: 256, g: 64, b: 0};
const setColor = (x, y, {r, g, b}) => {
const index = 4 * (size. w * y + x);
imgData [индекс + 0] = r;
imgData [индекс + 1] = г;
imgData [индекс + 2] = b;
imgData [индекс + 3] = 255;
}
for (let y = 0; y
Давайте посмотрим, как выглядит синхронная версия:
const writeImage = (холст, имя файла) => {
const out = fs.createWriteStream (__ dirname + '/' + filename);
const stream = canvas.createPNGStream ();
stream.pipe (выход);
out.on ('finish', () => console.log ('Файл PNG создан.'));
}
if (process. argv.length! = 9) {
console.log (`Использование: $ {process.argv [1]} [IMAGE_WIDTH] [IMAGE_HEIGHT] [PLOT_CORNER_X] [PLOT_CORNER_Y] [PLOT_WIDTH] [PLOT_HEIGHT] [FILENAME]`);
process.exit (0);
}
const size = {
w: + process.argv [2],
h: + process.argv [3]
}
const limits = {
x: parseFloat (process.argv [4]),
y: parseFloat (процесс.argv [5]),
w: parseFloat (process.argv [6]),
h: parseFloat (process.argv [7])
}
console.time ("всего");
const canvas = createCanvas (size.w, size.h);
const ctx = canvas.getContext ('2d');
const img = ctx.getImageData (0, 0, size.w, size.h);
img.data.set (plotMandelbrot (размер, границы))
ctx.putImageData (img, 0, 0);
console.timeEnd ("всего");
writeImage (холст, process.argv [8]);
Наша программа принимает некоторые аргументы от терминала, поэтому нам нужно запустить ее так:
узел mandelbrot2.js 1000 1000 -0,65 -0,72 0,1875 0,1875 test35.png
Если ваш узел имеет версию до 11.7.0, вам необходимо добавить флаг --experimental-worker
после узла
.
Вот результат вызова нашего скрипта с параметрами выше:
На моем ноутбуке потребовалось 317 секунд, чтобы нарисовать эту картинку. Конечно, мы можем сделать это намного быстрее, но программа, которую я написал, очень детализирована и требует больших вычислений, чтобы показать вам разницу между однопоточной версией и версией с Workers.А теперь давайте напишем это вместе с ними!
Рисунок параллельно
При работе с множеством потоков очень важно правильно спроектировать программу. Первая идея написания программы рисования с несколькими потоками может заключаться в том, чтобы разделить изображение на столько частей, сколько у нас есть Workers, и каждый Worker будет вычислять свою одну часть. Но стоит отметить, что вычисления могут быть не одинаковыми для каждой части изображения. В нашем случае более яркие части занимают гораздо больше времени, чем темные, поэтому, если бы у нас было два потока и мы разделили бы изображение по горизонтали пополам, у второго потока было бы гораздо больше работы, и мы бы ждали его, когда первый поток уже был бы завершен.
Второй вариант, который я считаю лучшим, - запустить несколько потоков вначале, разделить изображение на множество частей (например, полосы высотой 25 пикселей), а затем передать их потокам один за другим, как только они закончат. их предыдущая работа. Вычисления обрабатываются одинаково в каждом потоке, и нет необходимости ждать какой-либо поток, поскольку они будут выполнять свою общую работу почти одновременно.
Давайте посмотрим, как это можно реализовать с помощью Workers:
if (isMainThread) {
const {createCanvas, loadImage} = require ('холст');
если (процесс.argv.length! = 10) {
console.log (`Использование: $ {process.argv [1]} [IMAGE_WIDTH] [IMAGE_HEIGHT] [PLOT_CORNER_X] [PLOT_CORNER_Y] [PLOT_WIDTH] [PLOT_HEIGHT] [FILENAME] [THREADS]`);
process.exit (0);
}
console.time ("всего");
const canvas = createCanvas (size.w, size.h);
const ctx = canvas.getContext ('2d');
рабочие константы = новый набор ();
пусть workerCount = + process.argv [9];
пусть sheduledLines = 0;
const chunk = 25;
const finish = () => {
writeImage (холст, process. argv [8]);
консоль.timeEnd ("всего");
}
const runTask = (рабочий) => {
if (sheduledLines {
const img = ctx.getImageData (0, 0, size.w, кусок);
img.data.set (data.imgData);
ctx.putImageData (img, 0, data.pos);
runTask (рабочий);
});
runTask (рабочий);
}
}
else {
parentPort.on ('сообщение', (данные) => {
const imgData = plotMandelbrot (data.size, data.boundaries);
parentPort.postMessage ({
imgData,
pos: data.pos
});
});
}
Обратите внимание, что я добавил еще один параметр, считанный из процесса . argv
- количество потоков. Функции writeImage
, plotMandelbrot
и аргументы чтения остаются прежними.
У нас есть еще код, но его стоило написать. Вызов нашего скрипта с теми же параметрами, что и выше, и двумя потоками дает такое же изображение, сгенерированное всего за 164 секунды, по сравнению с 317 секундами в синхронной версии выше. Это почти в 2 раза быстрее! Но если вы думаете, что увеличение количества потоков ускорит процесс, вы ошибаетесь.Мои эксперименты показали, что с 3 потоками время очень похоже, а с более чем 3 становится еще хуже. Вероятно, это из-за проблемы, о которой я говорил выше. Потоки в Node дороги. На управление только потоком, даже без выполняемого кода, уходит много времени, а на наших компьютерах обычно не так много ядер.
Есть еще одна проблема, на решение которой у меня ушло больше часа. Не каждый пакет Node (например, из npm
) хорошо работает с Workers. У меня была проблема с холстом
. При использовании с частью кода Worker я получил странную ошибку:
Ошибка: модуль не саморегистрируется
Вначале я не знал, что происходит, но путем отладки методом проб и ошибок я обнаружил, что этот модуль нельзя использовать в Workers. Я провел небольшое исследование по этому поводу, проанализировал исходный код Node.js, просмотрел документацию по Node и, наконец, обнаружил, что могут возникнуть некоторые проблемы с использованием модулей, написанных на C / C ++, поскольку они плохо работают с несколькими контекстами. , как с рабочими.Проблема кажется довольно сложной, но если вы хотите узнать больше, вы можете прочитать об этом документацию.
Еще хорошие примеры!
Мне очень нравятся результаты программы. Вот некоторые результаты игры с ним, вы можете нажать на них, чтобы увидеть в лучшем качестве:
Еще я столкнулся с интересным провалом при написании параллельной версии.
И в конце то, что для меня похоже на солнце:
Многопоточность | Электрон
С помощью Web Workers можно запускать JavaScript в потоках уровня ОС.
Многопоточный Node.js
Можно использовать функции Node.js в веб-воркерах Electron, для этого параметр nodeIntegrationInWorker
должен иметь значение true
в webPreferences
.
let win = new BrowserWindow ({
webPreferences: {
nodeIntegrationInWorker: true
}
})
nodeIntegrationInWorker
можно использовать независимо от nodeIntegration
, но для песочницы
не должно быть установлено значение true
.
Доступные API
Все встроенные модули Node.js поддерживаются в Web Workers, а архивы asar
по-прежнему можно читать с помощью API-интерфейсов Node.js. Однако ни один из встроенных модулей Electron не может использоваться в многопоточной среде.
Собственные модули Node.js
Любой собственный модуль Node.js можно загрузить непосредственно в Web Workers, но настоятельно не рекомендуется этого делать. Большинство существующих собственных модулей были написаны для однопоточной среды, их использование в Web Workers приведет к сбоям и повреждению памяти.
Обратите внимание, что даже если собственный модуль Node.js является потокобезопасным, загружать его в Web Worker все равно небезопасно, поскольку функция process.dlopen
не является потокобезопасной.
На данный момент единственный способ безопасно загрузить собственный модуль - это убедиться, что приложение не загружает собственные модули после запуска Web Workers.
process.dlopen = () => {
выбросить новую ошибку («Загрузить собственный модуль небезопасно»)
}
let worker = new Worker ('script.js')
Видите что-то, что нужно исправить? Предложите изменить источник.
Нужна другая версия документов? Смотрите доступные версии или переводы сообщества.
Хотите искать сразу по всей документации? Смотрите все документы на одной странице.
Многопоточность
- каковы недостатки создания многопоточной реализации среды выполнения JavaScript?
1) Многопоточность чрезвычайно сложна, и, к сожалению, то, как вы до сих пор представили эту идею, подразумевает, что вы сильно недооцениваете, насколько это сложно.
На данный момент это звучит так, как будто вы просто «добавляете потоки» в язык и беспокоитесь о том, как сделать его правильным и производительным позже.В частности:
, если две задачи пытаются получить доступ к переменной одновременно, она помечается как атомарная, и они борются за доступ.
...
Я согласен с тем, что атомарные переменные не решат все, но моя следующая цель - работать над решением проблемы синхронизации.
Добавление потоков в Javascript без «решения проблемы синхронизации» было бы похоже на добавление целых чисел в Javascript без «решения проблемы сложения». Это настолько фундаментально для природы проблемы, что в принципе нет смысла даже обсуждать, стоит ли добавлять многопоточность, не имея в виду конкретного решения, как бы сильно мы этого ни хотели.
Кроме того, создание всех переменных атомарно - это то, что, вероятно, заставит многопоточную программу работать на хуже, чем на , чем ее однопоточный аналог, что делает еще более важным фактическое тестирование производительности на более реалистичных программах и посмотреть, получаете ли вы что-нибудь или нет.
Мне также не ясно, пытаетесь ли вы скрыть потоки от программиста node.js или планируете раскрыть их в какой-то момент, эффективно создав новый диалект Javascript для многопоточного программирования.Оба варианта потенциально интересны, но похоже, что вы еще даже не решили, к какому из них стремитесь.
Итак, в настоящий момент вы просите программистов рассмотреть возможность перехода с однопоточной среды на совершенно новую многопоточную среду, в которой нет решения проблемы синхронизации и нет свидетельств того, что она улучшает реальную производительность, и, по-видимому, нет плана решения этих проблем. .
Наверное, поэтому люди не воспринимают тебя всерьез.
2) Простота и надежность одиночного цикла событий
- огромное преимущество .
Программисты Javascript знают, что язык Javascript «безопасен» от условий гонки и других чрезвычайно коварных ошибок, которые преследуют все действительно многопоточное программирование. Тот факт, что им нужны веские аргументы, чтобы убедить их отказаться от того, что безопасность не делает их замкнутыми, а делает их ответственными.
Если вы не сможете каким-либо образом сохранить эту безопасность, любому, кто захочет переключиться на многопоточный node.js, вероятно, будет лучше переключиться на такой язык, как Go, который изначально был разработан для многопоточных приложений.
3) Javascript уже поддерживает «фоновые потоки» (WebWorkers) и асинхронное программирование, не открывая непосредственное управление потоками программисту.
Эти функции уже решают множество распространенных вариантов использования, которые влияют на программистов Javascript в реальном мире, не отказываясь от безопасности отдельного цикла событий.
Есть ли у вас какие-либо конкретные варианты использования, которые не решают эти функции, и для которых программисты Javascript хотят найти решение? Если это так, было бы неплохо представить многопоточный узел.js в контексте этого конкретного варианта использования.
П.С. Что могло бы убедить
меня попробовать переключиться на многопоточную реализацию node.js?
Напишите нетривиальную программу на Javascript / node.js, которая, по вашему мнению, выиграет от подлинной многопоточности. Проведите тесты производительности на этом примере программы на обычном узле и многопоточном узле. Покажите мне, что ваша версия в значительной степени улучшает производительность, скорость отклика и использование нескольких ядер во время выполнения, без каких-либо ошибок или нестабильности.
Как только вы это сделаете, я думаю, вы увидите, что люди гораздо больше заинтересованы в этой идее.
.