Как стать автором
Обновить

Комментарии 45

Всё бы хорошо — но пока нет возможности передавать объекты. А без этого фича не очень полезная. Но есть надежда, что допилят.

А где про это написано?

В пулл реквесте:

The super-high-level description of the implementation here is that Workers can share and transfer memory, but not JS objects (they have to be cloned for transferring), and not yet handles like network sockets.
Если это простые объекты, то JSON вам в помощь.
Ага, вместе с оверхедом на сериализацию и десериализацию. Но даже так не выйдет — увы, обычно есть какие-то сложные объекты, которые хочется унести на обработку в другой поток.

Уже который год жду, пока в ноду завезут сериализацию для полноценных объектов. Но увы — кроме редко работающего node-serialize от luin пока ничего нет, и это меня который год удивляет.

Если не копировать объекты, то получите состояние гонки. Данное решение необходимо чтобы избежать проблем одновременного доступа. Тот же Erlang использует копирование всегда, при этом считается очень производительным. При этом дочерний поток – это новый контекст, который ничего не знает о том как выглядит прототип объекта из другого контекста.


Этот же подход используется в WebWorker API и скорее всего будет применяться и в Node.js.

В Эрланге копирование всегда, но с важной оговоркой: бинарные данные размером больше 64 байт не копируются, а хранятся в общей куче и управляются по счётчику ссылок. А в виде бинарных данных в Эрланге принято обрабатывать почти все "данные". Для примера посмотрел на своем приложении:


  • binary: 709Мб
  • process: 124Мб
  • ets (ещё один способ шарить данные между процессами): 118Мб

Т.е. из 1Гб памяти потенциально может быть скопировано при передаче между процессами только 124Мб.


Ну и для копирования данных в Erlang под капотом используется memcpy а не сериализация в строку / json. Плюс собственная сложная система аллокаторов памяти заточенная на такое обращение.
Сокеты между erlang процессами можно передавать, но нужно для этого использовать отдельное API либо "обернуть" сокет в процесс. Тогда вообще никаких ограничений.

Спасибо за детальный ответ. Про IPC в статье указано, что будет использоваться механизм Worker API для передачи так называемых transferrable объектов, этот механизм оптимизирован в V8 (насколько это возможно) и хотя вряд ли сравнится с Erlang по производительности, как минимум из-за большей вариативности, это все же не обычная сериализация, так как передаваться будут и инстансы некоторых объектов.

Ну так SharedArrayBuffer тоже не копируется…

Так эрланг — это функциональщина, значит неизменяемость и возможность безболезненно шарить данные. Зато изменение через создание нового объекта, так что в одном месте копирования избежали, зато в другом получили

Есть блобы и blob-URL. Возможно ли их как то применить для этих целей?

Передавайте легкие обьекты в которых только данные нужные для вычисления. Зачем тянуть весь объект?

Это прекрасный совет, только одна беда — он из серии «нормально делай — нормально будет». В реальной жизни всё работает несколько по-другому — хотя бы потому что в большинстве случаев идёт работа с объектами, а не простыми данными. Хотя бы для того, чтобы использовать ссылки, которые позволяют не раздуть объект в 10 раз.

Я понимаю ваше разочарование но думаю возможнось передавать объекты добавила бы больше проблем и сложностей чем полезностей

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

Хотя возможно стоило бы разрешить передавать фрозен объекты только на чтение.

А все изменения в объект можно сделать асинхронными и выполнять их в главном потоке через event loop. Но пока есть что есть.

Этот подход сильно ограничен в применении. Например с таким подходом нельзя написать самую главную часть бекенда — базу данных. А база данных либо полностью либо часть данных в виде кеша хранит в некой структуре состоящую из объектов в оперативной памяти. И если на запись из-за race-condition можно разрешить обновлять данные только одному потоку то нет никаких причин не распараллелить чтение на все ядра процессора. Но только вот проблема — в ноде нет возможности читать к одну и ту же структуру объектов из разных потоков а хранить копию структуры в каждом потоке (а потом еще и синхронизировать изменения) никакой оперативки не хватит

Ну частично это можно обойти используя нод кластер и ращные библиотеки типо memored для общего доступа к структурам.

Да это точно потоки, а не что-то иное. Просто совместного доступа к памяти не будет, за исключением SharedArrayBuffer.

А в чем принципиальное отличие от child process api?

Различие API или плюсы перед child process?

Плюсы. Зачем нужен child process и как его готовить у меня представление есть. Про Worker API совсем ничего не читал. Хочется чуть более подробного введения, для чего и как его использовать, и в чем его преимущество над уже существующими решениями.

Некоторые плюсы я описал в статье (меньшее количество аллокаций памяти и более удобная и быстрая коммуникация между потоками), подробнее опишу, когда API финализируется и список получится более точным. Но для меня плюс в большем контроле: child_process может быть завершен другим процессом, а worker – нет.

А в чем проблема того, что другие процессы имеют теоретическую возможность убить дочерний процесс? К тому же они все равно могут kill / TerminateProcess по отношению к главному процессу.

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

Вот еще одна возможность многопоточности в Node.js — Napa.js
Как это все выглядит со стороны:
В мире java: многопоточность сложна, возьмем netty и будем писать в один поток!
В мире node: подержи мое саке, бака гайдзин!..
Node.JS появился и развивался как альтернатива системам построенным на концепции мультипоточного программирования. Теперь в node.js появляется возможность мультипоточного программирования. Может я чего не понимаю, но похоже что разработчики рубят сук на котором сидят. Нет?

Эта фича — она как child_process, только на потоках. Принципиально ничего нового.

Нет, это вполне себе улучшение.

С чем сравниваем ноду: с CGI/FastCGI, и пулом потоков(как в ASP.NET)

Юзкейс 1 — приложение в котором практически нет сложной вычислительной логики, а только операции ввода/вывода:
1) В CGI на каждый запрос создается и убивается отдельный процесс
Проигрываем на времени создания процесса, на памяти под отдельные процессы, и на переключении контекста между процессами
При этом большую часть времени каждый процесс находится в ожидании одной операции чтения/записи
2) В FastCGI процессы переиспользуются, и достаются/возвращаются из пула по мере необходимости.
Пропали потери на создание/уничтожение процесса, но память и переключение контекста остались
3) Пул потоков. Примерно тоже, что и в предыдущем пункте, только меньше накладных расходов на память и переключение контекста
4) Однопоточная асинхронная архитектура(Node.js, Twisted, ...)
Процесс и поток один. Нету накладных расходов на память и переключение контекста(если не считать соседние и системные процессы).
При условии что нету блокирующих вычислительных операций, все очень хорошо в сравнении с предыдущими пунктами

Юзкейс 2 — в приложении есть блокирующая вычислительная логика
Допустим время одного запроса 1 секунда, из них 0.1 — вычисления, и 0.9 — ожидание ввода вывода. И на сервер приходит в среднем 100 запросов в секунду
1-3) в среднем будет 100 одновременно активных процессов/потоков(с сопутствующими накладными расходами)
4.1) child_process/cluster. В среднем должно хватить 10 воркер-процессов, каждый из которых будет на 100% занят вычислениями(в перерывах между ними получая результаты ввода/вывода от других запросов)
Хотя возможна проблема когда приходят два одновременных запроса в один воркер, и тогда второму придется дождаться завершения блокирующей операции от первого запроса(+100мс для конкретного запроса).
Но можно увеличить количество воркеров(не столь значительно как для 1-3 подходов), и уменьшить потенциальный разброс
4.2) То же самое, только уменьшаются накладные расходы на процессы, и обмен сообщениями между ними
Таким образом Node.js и в данном юзкейсе хорошо справляется(используя меньшее количество процессов/потоков)
Уточнение: в ASP.NET давно уже можно использовать асинхронную архитектуру.
В том смысле что можно обрабатывать одновременные операции ввода/вывода в одном потоке, в событийном цикле, как это происходит в ноде?
Если да, звучит здорово.
Если же под отдельную операцию ввода/вывода нужно взять отдельный поток из пула, и работать с этой задачей асинхронно, то в плане нагрузки вроде ничего не меняется.
Асинхронные операции ввода/вывода вообще не привязываются ни к каким потокам. А вот их продолжения исполняются в пуле потоков.

Если возможность загнать все продолжения в один поток — но не в ASP.NET.

Кстати, для справки: механизм async/await в C# появился раньше чем в javascript (собственно, из C# его и стянули).
НЛО прилетело и опубликовало эту надпись здесь
Можно, никаких проблем
У веб-воркеров есть недостаток что невозможно как-то прервать вычисления (если задача вдруг стала неактуальной) не убив воркер полностью (это могло бы выглядеть как например бросание исключение работающему воркеру через worker.throw() чтобы прервать синхронные вычисления). Можно конечно прервать через worker.terminate() но создавать на каждую задачу новый воркер слишком медленно и нерационально а нужно иметь пулл потоков равный количеству ядер и распределять задачи между ними. В итоге в воркере нужно разбивать вычисления на асинхронные части и проверять проверять пришло ли сообщение на остановку задачи что сильно усложняет и замедляет вычисления в целом. Будут ли иметь этот недостаток веб-воркеры для ноды?
Судя по API — такой возможности пока нет. И, как мне кажеться, врядли будет.
А как бы вы эту задачу решали при использовании полноценных потоков в других языках :-)?
Насколько я знаю используя си на есть возможность послать сигнал процессу или потоку ( а потоки в линуксе тоже процессы просто разделяют адресное пространство) и это прервет синхронно вычисляемые задачи даже если там бесконечный цикл и управление сразу передастся обработчику сигнала и дальше уже он сможет либо вернутся к вычислениям либо прервать если задача стала неактуальной.

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

Не могу придумать, где это может пригодиться.

Например пришел запрос на вычисление cpu-bound задачи — мы отправили эту задачу в отдельный воркер (не создавая каждый раз новый и не убивая после завершения вычисления). Но тут вдруг запрос прерывается или сообщает что задача уже неактуальна а значить нет смысла ждать пока этот воркер закончит работу над этой задачей и надо как-то сообщить ему чтобы он прекратил вычисления. А исключение это просто как способ прервать какие-то синхронно вычисляемые задачи как например стек вызываемых функций или просто цикл.
Так вроде нигде нет такого функционала, например в c# реализован cancelation token, которые останавливает именно так как сказали.
Ну, кооперативное-то прерывание можно сделать без особых проблем через SharedArrayBuffer. А чужой код всегда может отловить исключение и сказать что его не было, так что тут только прибивать воркер и остается.
Зарегистрируйтесь на Хабре, чтобы оставить комментарий

Публикации