Прежде всего, я хочу поблагодарить команду Nordic.js за гостеприимство, и я хочу поблагодарить каждого человека, которого я встретил до сих пор! Спасибо за поддержку и за отличную поддержку!

Если вы посещали Nordic.js 2016 или смотрели прямую трансляцию, вы знаете, как я боролся с моей презентацией.

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

Это была моя самая большая ошибка. Урок выучен!

Итак, я решил опубликовать то, что я не мог сказать вслух, чтобы вы, по крайней мере, могли прочитать то, что я сказал, но не мог выразить в то время.

Слайды теперь общедоступны здесь:



Добрый день всем! Меня зовут Абдулла Али. Я здесь впервые, и я хочу поблагодарить вас всех за то, что пришли сегодня.

Сегодня я собираюсь рассказать о своем новом проекте Nexus.js, который направлен на привнесение многопоточности в мир JavaScript.

Но чтобы поговорить о Nexus, мы сначала должны поговорить о Node.

И чтобы поговорить о Node, мы сначала должны поговорить о самом JavaScript.

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

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

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

Цикл событий - это однопоточная конструкция, которая позволяет вашему коду иметь «побочные эффекты». Такие вещи, как setTimeout и setInterval, являются хорошим примером побочных эффектов.

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

Давайте посмотрим на типичную консольную программу: она запускается, выполняет одну задачу, а затем выходит с кодом возврата.

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

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

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

Итак, как бы вы внедрили настоящий параллелизм в такую ​​систему?

Nexus начинался как простой эксперимент, и я спросил себя: есть ли какие-нибудь движки JavaScript, которые позволяют вызывать JavaScript из нескольких потоков параллельно?

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

Моим вторым вариантом был SpiderMonkey от Mozilla, и я не буду вдаваться в подробности. Достаточно сказать, что все прошло плохо. Все рухнуло, куча была повреждена, и ничего не работало дольше нескольких секунд без окончательного сбоя.

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

Затем я попробовал JavaScriptCore - движок, используемый WebKit, - и я осмелюсь сказать, что это самый продвинутый движок JavaScript, с которым я когда-либо сталкивался! И это чудесным образом сработало как шарм!

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

Это позволяет нам вызывать любой контекст JavaScript из нескольких потоков параллельно, безопасно и без опасений за стабильность программы.

Следующим моим шагом было преобразование цикла событий, который представляет собой однопоточную конструкцию (что-то вроде цикла while), во что-то, что может работать параллельно.

Здесь я откопал старую реализацию планировщика пула потоков, которую написал в 2013 году для старой игры, над которой я работал, она была написана на C ++ с использованием boost.

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

Затем я интегрировал пул потоков в Nexus, и вместо однопоточного цикла while он запускает множество циклов в нескольких потоках, где каждый поток выбирает первую доступную задачу и выполняет ее, а другой одновременно выбирает следующую и т. Д. и так далее.

Конечным результатом является действительно параллельная среда выполнения JavaScript.

Теперь достаточно поговорить о C ++ стороне вещей, я хочу поговорить о JavaScript стороне вещей.

Используя Node, вы можете запланировать немедленный запуск задач на следующей итерации цикла событий, используя setImmediate или process.nextTick, или (классически) в определенное время или интервал, используя setTimeout и setInterval.

В Nexus все запланировано в пуле потоков, и чтобы использовать его напрямую, вы используете Nexus.Scheduler.schedule ().

Эта функция принимает единственный аргумент - функцию для планирования - и помещает его в очередь событий для выполнения следующим доступным потоком.

setTimeout и setInterval также планируют в очереди событий пула потоков, но позволяют вам установить время или интервал для выполнения.

Это самый простой и низкоуровневый метод доступа к планировщику задач, но вам не нужно использовать его напрямую.

Поскольку Nexus основан на ES6, у вас есть гораздо лучшее оружие: действительно асинхронные обещания, которые выполняются на всех ядрах параллельно.

Обещания ES6, по сути, представляют собой способ представления изолированных асинхронных операций.

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

Это приводит к интересному наблюдению: выполняющиеся параллельно обещания редко взаимодействуют или конкурируют за доступ к ресурсам (если вы избегаете использования глобальных или общих переменных).

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

Одним из аспектов реализации многопоточного обещания является то, что вызовы Promise.all () и Promise.race () будут планировать все сразу, а затем ждать разрешения результатов.

Когда вы планируете 1000 обещаний в Node, он фактически будет оценивать их синхронно, одно за другим, в цикле событий из-за своей неспособности распараллелить JavaScript.

С Nexus вы автоматически получаете преимущество от того, сколько логических процессоров работает в вашей системе, например: с восемью логическими процессорами восемь обещаний будут выполняться одновременно.

Вот почему я решил реализовать весь API Nexus с помощью обещаний, включая Event Emitter.

Генератор событий - это основная конструкция Nexus, которая несколько отличается от реализации генератора событий по умолчанию в Node.

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

Он делает это, возвращая обещание, когда вы вызываете `emit`, это обещание разрешается в тот момент, когда все обработчики возвращаются, что позволяет вам связывать события.

Еще одно важное отличие состоит в том, что все обработчики вызываются параллельно, что позволяет максимально использовать многопоточность.

Это дает ему преимущество в производительности, что напрямую влияет на асинхронный ввод-вывод.

IO в Nexus немного отличается от Node, поскольку он разделяет время ЦП с JavaScript в пуле потоков.

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

Это может стать проблемой, когда вы выполняете много операций ввода-вывода. Таким образом, Nexus предлагает два разных типа входных моделей. Модель Pull, которая позволяет вам читать данные, вызывая метод read, и модель Push, которая основана на событиях и имеет высокую пропускную способность, и она работает, вызывая «resume» на устройстве ввода.

Но подожди секунду. Что такое устройство ввода?

Интерфейс ввода-вывода для Nexus смоделирован по образцу библиотеки boost ASIO.

Фактически, многие дизайнерские идеи, используемые в Nexus, смоделированы после ускорения, что обеспечивает проверенные временем конкретные модели для таких вещей, как ввод-вывод, сети и IPC.

Итак, предпосылка: у вас есть устройства и есть стримы.

Устройства - это самые простые строительные блоки любого графа ввода-вывода. Есть три типа устройств: читаемые, записываемые и двунаправленные.

Потоки позволяют обрабатывать данные на более высоком уровне. Каждый поток требует, чтобы устройство функционировало. Есть два типа потоков: читаемые и записываемые.

Читаемый поток требует читаемого или двунаправленного устройства.

Для записываемого потока требуется записываемое или двунаправленное устройство.

Оба типа потоков принимают фильтры, которые управляют любыми проходящими буферами.

Здесь стоит отметить одну вещь: все, что поступает или поступает на устройства, является двоичным. Nexus использует объекты ArrayBuffer для передачи пакетов данных.

Это позволяет Nexus исключить накладные расходы на копирование памяти. Объекты ArrayBuffer становятся владельцами любых переданных в них буферов памяти.

На этом экране вы можете увидеть базовый график ввода-вывода, который читает из FilePushDevice и направляет вывод в FileSinkDevice через потоки.

Итак, давайте взглянем на код.

В этом примере файл считывается с диска, преобразуется из UTF-8 в UTF-16 и одновременно направляется вывод в четыре потока вывода.

Он ожидает завершения операции, затем выводит общее время, затраченное на выполнение операции.

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

Итак, каковы большие преимущества этого дизайна?

  • Более высокая производительность.

Приложения Nexus.js могут полностью использовать логические процессоры.

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

  • Динамическое масштабирование на современном оборудовании.

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

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

  • Максимальное использование ЦП и памяти.

Требования к памяти для масштабирования приложения значительно снижаются.

Для приложения с ОЗУ размером ~ 1 ГБ, работающего в 4-ядерной системе:

Узел: 4 * ~ 1 ГБ = ~ 4 ГБ ОЗУ для 4 процессов

Nexus: ~ 1 ГБ для одного процесса

Спасибо за чтение!

Вопросы?

У вас есть вопросы? Буду рад на них ответить. Присылайте их здесь, в комментариях, или в @voodooattack в Твиттере, и как всегда: вы можете просмотреть код проекта на GitHub.