progcode_D / PHP / WEB

D

Особенности Vibe.d

Подробный обзор архитектуры vibe.d: волокна и события, сетевые API, рабочие потоки, метапрограммирование D и шаблоны Diet.

Обзор основан на переводе страницы возможностей vibe.d, опубликованном в 2017 году. Он объясняет архитектурные идеи: волокна, цикл событий, последовательный API и средства D. Перечень встроенных драйверов, опций и обещаний производительности относится к версии того времени; перед применением сверяйте API установленного пакета.

Простота

Асинхронный ввод-вывод с последовательным кодом

Основная идея vibe.d — совместить эффективный асинхронный ввод-вывод (AIO) с последовательным кодом, который удобно читать и сопровождать. В модели с обработчиками событий, характерной для многих Node.js-программ того времени, завершение операции обрабатывается функцией обратного вызова. Хотя это приемлемый подход, он имеет ряд недостатков.

Во время ожидания IO один fiber приостанавливается, а event loop выполняет другой
Псевдоблокирующий стиль скрывает ожидание I/O; вычислительная работа сама по себе не становится неблокирующей.

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

При переходе между callback-вызовами обычный стек не всегда отражает всю логическую последовательность запроса. Это усложняет отладку; исключение нельзя просто передать назад через уже завершённый вызов. Современные средства языка могут облегчать эту задачу, поэтому историческое сравнение нельзя считать универсальной оценкой Node.js.

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

Для этого используются волокна — сопрограммы с собственным состоянием выполнения. Несколько волокон могут по очереди исполняться в одном потоке ОС; отдельное волокно не следует путать с отдельным процессорным ядром. Когда выполняющееся волокно вызывает функцию yield(), управление возвращается запустившему его коду. Затем выполнение продолжается с сохранённой позиции и состояния, на которых произошёл вызов yield(). Так планировщик чередует волокна: пока одно ожидает ввод-вывод, другое может выполнять полезную работу.

Эти переключения обычно скрыты за API vibe.d, поэтому обработчик запроса остаётся последовательным. Функции ожидания, такие как sleep() или read(), приостанавливают текущую задачу и позволяют возобновить её при наступлении события; это не обещает аналогичного поведения произвольной функции сторонней библиотеки.

Такой API скрывает значительную часть механики событий от прикладного кода. Он может сочетаться с многопоточностью, но для передачи данных между потоками по-прежнему нужен определённый контракт. Все параллельные операции выполняются с использованием так называемых задач. Каждая задача выполняется внутри волокна, а планировщик чередует её с другими задачами. Однако задача и поток ОС — разные сущности; это важно для блокировок и вычислительной нагрузки.

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

Компактный API

Одна из целей библиотеки — короткий и понятный прикладной код. Типовые задачи выражаются высокоуровневыми вызовами, при этом для особых случаев остаётся низкоуровневый доступ. Шаблоны и другие возможности D помогают сокращать повторяющийся код, не отказываясь от статической типизации.

Переключение между версиями процесса

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

Обработка ошибок на основе исключений

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

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

Исключения помогают явно передавать ошибку вверх по стеку. Но они не гарантируют безопасность: исключение всё равно нужно обработать на подходящей границе, а чувствительные подробности не должны попадать в публичный ответ. Для HTTP-сервиса настройте обработку и журналирование ошибок так, чтобы пользователь получил корректный ответ, а диагностические данные остались в журнале.

Преимущества

Инструменты веб-фреймворка

Библиотека vibe.d содержит полный набор инструментов, необходимых для разработки веб-сайтов и веб-сервисов. В дополнение к HTTP 1.0/1.1 серверу предусмотрено обслуживание статических файлов, эффективная система шаблонов, веб-сокеты, сеансы и другие функции. К числу таких функций относятся фильтр markdown, драйверы MongoDB и Redis, криптография, поддержка JSON и BSON и простой SMTP-клиент.

Есть также средства для автоматической генерации JSON / REST или HTML-интерфейсов на основе форм из классов D, что сокращает повторяющийся связующий код. Генератор интерфейса REST поддерживает генерацию как сервера, так и клиентской части, поэтому может использоваться для RPC-подобного взаимодействия.

Встроенные драйверы баз данных

Основная библиотека содержит встроенную поддержку баз данных MongoDB и Redis. Их наличие упрощает интеграцию, но не определяет само по себе скорость и надёжность хранения. Дополнительные драйверы базы данных доступны в реестре пакетов DUB, например, совместимый с vibe.d драйвер MySQL.

Последовательный вид API облегчает адаптацию некоторых существующих драйверов. Один из возможных шагов — заменить вызовы сокетов (send (), recv (), connect () и т. д.) соответствующими функциями vibe.d. Но затем необходимо проверить блокирующие операции, время жизни соединений и поведение при ошибках; одной замены имён функций недостаточно.

TCP, UDP и файловый ввод-вывод

Доступ к TCP, UDP и файлам позволяет строить собственные протоколы и обрабатывать форматы данных поверх низкоуровневого ввода-вывода. Прикладной код работает с последовательным интерфейсом, хотя внутри используются события и ожидание готовности.

Общие инструменты параллелизма

Помимо операций ввода-вывода поддерживаются все обычные инструменты для общих задач программирования:

  • Ожидание — приостанавливает текущую задачу на заданное время, позволяя выполнять другие задачи
  • Таймеры позволяют асинхронное планирование обратных вызовов
  • Мьютексы и условные переменные позволяют осуществлять совместный доступ к данным между несколькими потоками без непреднамеренной блокировки цикла событий при использовании соответствующих библиотечных примитивов
  • Поддержка передачи сообщений
  • Каналы данных для потоковой передачи данных между задачами и потоками

Интеграция графического интерфейса пользователя

Модель событий может быть полезна и в графическом приложении, если сетевые события согласованы с циклом интерфейса. Историческое описание vibe.d приводило примеры такой интеграции для Windows и X11.

Для Windows в обзоре описывался драйвер VibeWin32Driver, использовавший MsgWaitForMultipleObjectsEx для обработки оконных сообщений вместе с событиями ввода/вывода или параллелизма. Для X11 приводился подход с createFileDescriptorEvent: готовность соединения с дисплеем включается в общий цикл событий вместо отдельного блокирующего ожидания XNextEvent.

Производительность

Асинхронные операции ввода-вывода

Асинхронные API операционной системы позволяют ожидать несколько сетевых или файловых операций без выделения отдельного потока на каждое ожидание. В описании 2017 года в качестве переносимого слоя упоминалась libevent; используемый драйвер нужно уточнять для версии приложения.

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

Другим важным аспектом для скорости является то, что, поскольку требуется только один или несколько потоков, можно уменьшить число дорогих переключений потоков ОС. Однако, если приложение должно выполнять много вычислений помимо операций ввода-вывода, vibe.d имеет полную поддержку многопоточности, чтобы полностью использовать многоядерный процессор системы.

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

Поддержка многопоточности

Один поток удобен для большого числа ожиданий, но длительные вычисления могут требовать нескольких ядер. Vibe.d поддерживает несколько способов использования этой дополнительной вычислительной мощности, оставляя решение по архитектуре потоков программисту.

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

Выбор архитектуры многопоточности остаётся за приложением. Система типов D и библиотечные средства передачи данных помогают ограничивать совместный изменяемый доступ, но это не универсальная гарантия отсутствия гонок: небезопасные преобразования и __gshared требуют отдельного анализа. В следующем списке показаны некоторые типичные архитектуры потоков:

  • Распределенная обработка входящих соединений

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

  • Использование рабочих задач для вычислений

Вычислительно дорогие задачи могут быть выгружены из основного потока, выполнив их с помощью runWorkerTask. Любая такая задача будет выполнена в пуле потоков. Этот подход полезен в тех случаях, когда приложение требует выполнения таких дорогостоящих задач, потому что длительный расчёт не занимает основной поток, обслуживающий ввод-вывод. Вероятно, наиболее распространенным примером такой задачи является обработка/декодирование/кодирование изображений.

  • Использование простых D-потоков

Обычные потоки D также могут использоваться вместе с vibe.d. Это важно, когда используются потоковые примитивы стандартной библиотеки D. Помимо основных потоков D, это std.parallelism и std.concurrency. Обратите внимание, что части стандартной библиотеки D не следует смешивать с функциями ввода-вывода vibe.d, поскольку они блокируют цикл обработки событий. В историческом обзоре отдельно предупреждалось о блокирующих средствах std.concurrency и предлагалось использовать совместимые функции vibe.core.concurrency. Конкретную замену выбирайте по версии API и требуемой модели ожидания.

Обратите внимание, что vibe.d также включает в себя экспериментальные средства, поддерживающие изолированные и ограниченные ссылки. Это позволяет передавать изменяемые данные между потоками без произвольного совместного доступа к одним и тем же изменяемым объектам. Это особенно полезно при передаче данных, используемых в рабочих задачах между рабочим потоком и основным потоком.

Компиляция D в машинный код

Vibe.d написан на языке программирования D. D – это C-подобный язык, который компилируется в машинный код. Производительность определяется конкретной программой; практический интерес для vibe.d представляют также средства метапрограммирования, типизации и работы с памятью.

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

  • Шаблоны, миксины и compile-time функции

Метапрограммирование позволяет переносить повторяющуюся работу из ручного кода в компиляцию. Шаблоны (сравнимые с шаблонами C ++) поддерживают вариадические параметры, а также параметры строк и параметры-псевдонимы (alias) – это средства для формирования интерфейсов на этапе компиляции.

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

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

  • Сборщик мусора

Сборщик мусора D управляет временем жизни выделенных им объектов и уменьшает объём ручной работы с памятью. Он не исключает удержание ненужных ссылок и не заменяет правила владения внешними ресурсами: файл или сетевое соединение тоже нужно вовремя закрыть.

Отдельное полезное средство языка — неизменяемые данные. Если объект действительно immutable, его содержимое не меняется после создания, что упрощает совместное чтение из разных потоков. При передаче данных между потоками важно соблюдать ограничения типов и API передачи сообщений. Это помогает ограничивать класс ошибок, связанных с конкурентной записью, но не отменяет анализ остального разделяемого состояния.

  • Замыкания и лямбда-функции

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

  • Свойства

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

  • Модульная система
    Код D организован в модули, связанные явными импортами. Среди прочего он описывает зависимости между различными модулями (D-файлами). Используя эту информацию, становится возможным создавать целые приложения, просто указывая корневой файл (app.d) в командной строке. Зависимости можно найти рекурсивно. Инструменты rdmd и DUB помогают организовать такую сборку; точные команды зависят от выбранного инструмента и проекта.
  • Массивы и срезы

Для массивов предусмотрен удобный синтаксис срезов. Совместно со сборщиком мусора они позволяют удобно представлять участки строк без обязательного копирования. Границы срезов и время жизни исходных данных всё равно нужно учитывать; настройки компиляции и безопасный поднабор языка влияют на проверки.

  • Краткий и читаемый синтаксис

Дизайн API стремится использовать выразительный синтаксис D без лишних служебных конструкций. Лаконичность важна и в сравнении со скриптовыми языками, но её лучше оценивать по конкретному коду. Ключевое слово auto позволяет вывести тип из инициализатора, не превращая программу в динамически типизированную. Используйте вывод типов там, где он улучшает читаемость, а не скрывает важный контракт.

Компилируемый шаблонизатор “Diet”

Vibe.d имеет встроенную поддержку HTML-шаблонов Diet на основе синтаксиса шаблона Pug, известного из node.js, который, в свою очередь, основан на Haml. Эти шаблоны сокращают синтаксические накладные расходы, которые присущи нотации HTML-тегов, и допускают встраивание динамических элементов. Как правило, эти элементы написаны на языке динамических скриптов (JavaScript в случае Pug).
Однако в vibe.d шаблоны могут содержать встроенный код D. Шаблоны читаются и анализируются, пока приложение компилируется и оптимизируется. Для них создается код на D. Это означает, что не требуется заново разбирать файл шаблона для каждого запроса – не осуществляется доступ к диску, не выполняется синтаксический анализ, и генерация результата выполняется уже скомпилированным кодом; само формирование HTTP-ответа всё равно имеет стоимость.
И шаблон, и остальное приложение используют D: это даёт единый набор типов и инструментов, но требует внимательно отделять данные страницы от генерируемой разметки.

Перевод https://vibed.org/features

Как применять эти идеи на практике

Разделите три типа работы: ожидание сетевого события, короткое преобразование данных и длительный расчёт. Ожидание отдавайте совместимому асинхронному API, а вычисление не оставляйте надолго в потоке цикла событий. Если сторонняя библиотека блокирует поток, вид «синхронного» кода не делает её автоматически кооперативной.

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

КОНЕЦ ЗАМЕТКИ↑ В начало