Комментарии 17
Есть пару вопросов автору:
1. В статье указано, что арена полезна, чтобы избежать создания множества мелких Uint8Array при сериализации в бинарный формат. Тут сразу вопрос зачем нужно вообще этим заниматься. Но даже если это и нужно не будет ли в этом случае проще и эффективнее использовать стандартный пул переиспользуемых TypedArray? Ручное управление смещениями (offsets) в ArrayBuffer усложняет код, а выигрыш по сравнению с переиспользованием одного Float64Array/Uint8Array из пула кажется сомнительным.
2. Вы упомянули, что p-limit не интегрирован с корутинами и приоритетами. Это справедливо, но для 90% сценариев ограничения параллелизма (например, лимит запросов к API) приоритеты не нужны. Не получается ли, что для типовых задач ваше решение является избыточным по сравнению с тем же p-limit, который работает с обычными async/await без перехода на тяжелые генераторы?
3. В статье упоминается возможность использования SharedArrayBuffer и воркеров, но не раскрыта мотивация. Какие конкретные кейсы оправдывают использование, если для большинства задач "тяжёлых вычислений" достаточно обычного пула Web Workers с явной передачей сообщений через postMessage?
На самом деле для 90% бытовых задач, возможно вся эта возня покажется избыточной. Пожалуй, для наглядности отвечу с примерами.
1) Про пулы и TypedArray, покажу на примере игровых серверов.
Представим, что у нас есть популярная онлайн игра в вебе, где каждый тик сервер должен:
Принять и разобрать тысячи различных пакетов.
Для каждого пакета выделить временные структуры: координаты игрока, векторы движения, состояния анимации.
Сформировать и отправить обновления всем игрокам в зоне видимости.
Если для каждого пакета создавать новый DataView или Uint8Array, за секунду набегают миллионы мелких аллокаций, здесь у нас будут фризы из-за сборщика мусора. Если же мы возьмем пул TypedArray - это поможет лишь частично, потому что часто пакеты передаются не фиксированной длинны и если нужны буферы побольше - то либо придется держать пулы разных размеров, усложняя код, либо выделять память, а в контексте онлайн-игр - это не есть хорошо.
Арена решает этот кейс:
Выделили блок на 1 КБ из арены, записали данные, отправили, и забыли.
В конце обработки пакета арена автоматически сбрасывается, без необходимости вручную возвращать каждый буфер.
Можно выделить переменный размер без ограничений пула.
TypedArray в общем хорош для простых однотипных данных, но если нужны переменные размеры, множество разных данных временных, то он окажется не удобным.
2) Про p-limit - в большинстве типовых задач да, хватит и его, рассмотрим пару кейсов, где с ним будет сложно:
Пользователь листает историю чата, и ему прилетает новое сообщение - если использовать голый p-limit, то оно останется в очереди внизу и дойдет только после всех await от получения истории (нет приоритета).
В онлайн играх через один сокет летит огромное количество разноплановых данных - это может быть аналитика, передвижение игроков, рассчет попаданий, статус игрового мира, и т.д. Без приоритезации - игроки вместо получения например данных о своём перемещении, будут получать аналитику вперед, что абсолютно не нужно.
Пользователь покинул чат, а сервер продолжает держать отложенные сообщения для рассылки для других пиров - тут помогают корутины с отменами всего древа. Корутины могут быть вложенные и отмена одной - отменяет всё древо, а при классическом async / await - древо придется отменять вручную каждый элемент, что не удобно.
Короче говоря, в простых приложениях с простой логикой - p-limit хорош для простого ограничения параллелизма, но когда появляются требования к приоритетам, отмене и кооперативности, корутины становятся более подходящим инструментом.
3) SharedArrayBuffer нужен для того, чтобы не передавать состояния из воркера в воркер, когда нужно разным задачам работать с одними данными. Для наглядности приведу примеров:
Обработка видео в реальном времени (например, на лайв стримах). Два воркера могут работать с одним и тем же видео-кадром, не гоняя между собой данные, например - один воркер накладывает какой-то фильтр, другой анализирует в кадре жесты, чтобы запустить "emoji" в чат.
Разделяемая память для игрового состояния (позиции объектов, физика), чтобы воркеры могли параллельно обновлять разные части мира, не пересылая данные туда-сюда.
Если корутины в разных воркерах должны обмениваться сообщениями через каналы, то SharedArrayBuffer позволяет реализовать эти каналы с минимальными накладными расходами.
Если задачи изолированы, обмен редкий, и данные небольшие postMessage хватит для обмена данными, в остальных случаях используем SharedArrayBuffer.
У меня реализован класс DistributedScheduler, он может использовать общую арену для балансировки задач между воркерами.
Без обид, но выглядит так, что вы забиваете микроскопом гвозди. Делать онлайн-игру на JS-рантайме, кажется как раз тот случай, когда и так приходится бороться с массой ограничений среды. Накидывать сверху свой кастомный рантайм на генераторах, который далеко не zero-cost, в таких условиях только усугубляет проблему, а не решает её.
Ну про микроскоп и гвозди, в некоторых случаях это так, где например, async/await работают на ура, но есть случаи, где нужна приоритезация, удобная отмена всей цепочки вызовов, без блокировки event-loop-а, особенно на realtime с частыми вызовами тиков, где нужно уметь уступать для более важных вызовов.
Да, у генераторов есть оверхед, но вопрос в его оправданности, где нужно увеличивать / уменьшать приоритет, снижать нагрузку на сборку мусора для исключения фризов на приоритетные задачи, управлять синхронизацией. Долгое время сам V8 был на генераторах.
Ну и проблема здесь не усугубляется, если нам нужна полная предсказуемость памяти, миниум блокировок event-лупа, приоритезация и обработка пачками.
Если же мы берем игры как пример, то такой подход помогает:
1) Убирать утечки памяти в древовидных запросах (например, игрок вышел из игры, но до этого был запущен запрос, внутри которого была пачка других обработок, все их нужно как-то нормально отменить, не спровоцировав сборки мусора, без падений сокетов).
2) Решать вопросы голодания, когда мы кучой игроков разом начинаем выполнять действие (начался новый раунд, нужно всем допустим обновить статус игроков), в случае с async await, они все грохнуться в эвент луп, обработка следующего тика задержится, и все получат зависание.
3) async await также не бесплатный, как и генераторы, при множестве маленьких вычислений в реалтайме, поток будет останавливаться на убийство всех Promise.
4) Каждые несколько секунд сервер выполняет тяжелую задачу (например, агрегация и отправка логов, или сохранение снапшота текущего состояния комнаты в игре), то при обычном подходе, вы не можете эту тяжелую операцию разрубить на несколько подходов, произойдет фриз, а с приоритетами и корутинами - разбили её на воркеры + понизили приоритет выполнения, чтобы изначально игрокам отдавались важные данные, а потом уже шла работа над фоновыми задачами.
Кто не понял - это его библиотека.
Спасибо, кэп 😁
Одну статью открываешь, там: я, я, я через слово, вторую: моя, моей и так далее по падежам - где суть? - да покороче. А то одни местоимения и запах комплексов.
\s Все верно, нужно вообще ничего не писать, чтобы всякие местоимения глаза не мозолили, правда @historydev?
Вам не кажется, что человек, который проделал титанический труд и выложил его в открытый доступ, имеет право говорить, что это его работа?
Мне кажется, статья о том, как придумать проблему там где её нет и героически решить, добавив кучу абстракций. Кооперативная многозадачность, например, нужна тогда когда у нас один вычислительный ресурс и куча задач. Но для серверов это категорически неправда - у нас куча ядер процессора и мы можем раскидывать вычисления как нам удобно по ним. Кооперативная многозадачность тут превращается в трудно отлаживаемый оверхед, увеличивая среднее время ответа на запрос без веской причины, потому что выполнение вычислений прерывается на что-то другое.
В 90% случаев, вы правы. В комментарии выше, где были вопросы, я расписал кейсы, где эти проблемы имеются.
Моя идея остаётся: я считаю что ваши прикладные проблемы можно и нужно было решать с учётом архитектуры JS runtime и видимо, глубоко изменив подход, но не пытаться заставить рантайм эмулировать то для чего он не проектировался. Я не знаю деталей вашей прикладной проблемы, но уже сталкивался с подобным наслоением абстракций - каждый раз оказывалось, что абстракции появлялись либо вследствие желания применить простаивающие знания, или из-за того что человек пытается подстроить экосистему под что-то ему уже привычное вместо того чтобы проектировать под то с чем работает. Независимо от случая я почти уверен что существуют более простые и надёжные решения.
Да, и вы будете правы, для большинства случаев, однако.
1) Даже в многоядерных серверах каждый отдельный Node.js-процесс, это однопоточная среда, где одновременно выполняется только одна задача. async/await базово реализует кооперативную многозадачность, но проблема в том, что он уступает только Promise, без возможности остановки в произвольных точках.
2) Рантайм не эмулирует того, для чего не проектировался - генераторы поддерживаются рантаймом, специально созданы для реализации, например, итераторов. Мы лишь поверх ставим понятное управление над всем этим.
Примерами того, где используются такие подходы:
1) Онлайн игры, где уж вы никак не обойдетесь без наслоения абстракций, и тем более без управления приоритетами задач.
2) Такой подход можно использовать и в чатах / мессенджерах, например, виджеты чатов на сайтах, если берем классический веб. Понятно, что там есть и другие подходы распределения и отмены разных тасок, но бывают, сложные задачи, где так же нужны приоритеты или пакетная обработка кучи памяти (онлайн-трансляции).
Для простых задач вам в это всё погружаться как правило и не стоит.
Отчасти вы меня убедили, но тогда встаёт другой вопрос - зачем брать для проблемы среду, которая заведомо плохо подходит? Подобные вещи как-то логичнее писать на плюсах или расте, не рискуя получить фризы из-за GC
Безусловно, если нужна максимальная производительность - тут выступает Rust или плюсы, однако это уже другой вопрос, вопрос выбора самого языка для сервера, допустим. Только в реальности, выбор языка не сводиться только к его производительности, иначе всё было бы написано на Rust / C++ поголовно. Я бы и сам мечтал о таком, но веб-серверы, чаты, браузерные игры, инструменты автоматизации, часто пишутся на других языках. Это дешевле, проще и быстрее. И это дает отстрелы в колени, которые нужно как-то нивелировать. Тут же еще решают сроки, бюджеты, сложность поиска разработчиков и т.д.
Typescript - хорошее решение для балланса всего этого, включая общую кодовую базу с фронтендом, и это достаточно мощная среда даже для больших вычислениях, при маленьких бюджетах, просто её нужно правильно раскрыть.

Управляемые корутины в TypeScript: как получить контроль над асинхронностью и памятью. Разбираемся во всех аспектах