Одной из ключевых возможностей SObjectizer-5, которой не наблюдается у аналогов вроде C++ Actor Framework и Just::Thread Pro, является возможность запустить несколько независимых друг от друга экземпляров SObjectizer Environment внутри одного приложения. Грубо говоря, в CAF всего один ран-тайм на приложение, а в SO-5 таких может быть сколько угодно.
Важность такой фичи была хорошо прочувствована на собственной шкуре еще во времена SObjectizer-4, в котором ран-тайм был как раз один-единственный на приложение. Мы тогда сделали пару прикладных библиотек для заказчиков. Снаружи был обычный C++, а вот в потрохах, глубоко упрятанные от пользователя, работали агенты SO-4. Было удобно: мы легко пишем многопоточный код, клиенты ничего не знают про SObjectizer. Но эта благодать продолжалась не очень долго...
В один прекрасный момент нам потребовалось задействовать одну из таких библиотек внутри своего же SObjectizer-приложения. И тут выяснилось, что и приложение хочет управлять SO-4 ран-таймом, и библиотека. А потому они мешают друг другу. Пришлось выпускать новую версию библиотеки, в которой можно было указать опцию работать внутри уже имеющегося ран-тайма. Выглядело, да и являлось, все это настоящими костылями. Поэтому в самом начале работ над SO-5 была поставлена цель обеспечить возможность существования и независимой работы нескольких ран-таймов внутри одного приложения.
Так появилось понятие SO Environment, т.е. контейнера, который содержит ран-тайм и все кооперации с агентами, создаваемые пользователем.
Судя по отзывам на профильных форумах, понятие SO Environment как-то туго заходит. Уж не знаю почему. Может народ привык к приложениям в Erlang стиле, где есть всего один ран-тайм и абсолютно все живет внутри него. Может потому, что наличие SO Environment повышает многословность SObjectizer-овского кода, особенно если сравнивать с CAF/Just::Thread. Может потому, что в C++ модель акторов пока используется редко и в специфических областях.
Как бы то ни было, SO Environment уже есть, со своей задачей успешно справляется. Поэтому единственное, что остается, -- это пытаться на пальцах объяснять, где наличие нескольких экземпляров SO Environment может быть выгодно.
Для этих целей в состав разрабатывающейся сейчас версии 5.5.9 включен новый пример convert_lib. Этот пример показывает, как может выглядеть библиотека, которая выставляет наружу plain-old-C интерфейс, а внутри себя использует SO Environment. Пример игрушечный, демонстрационный. Но показанные в нем принципы работают и в реальных условиях.
В примере convert_lib показана библиотека для конвертации целых чисел из строкового представления в числовое. Все очень примитивно, но для демонстрации самое то. В реальной жизни может быть что-то посерьезнее, например, поддержка криптографии, общение с внешним оборудованием, тяжелые числодробилки и т.д. Целью примера было показать, как это все можно делать и конвертация чисел, имхо, подходит весьма неплохо.
Интерфейс "библиотеки" типичный для plain-old-C библиотек. Сначала посредством create_converter нужно создать экземпляр конвертера и получить его хендл. Когда конвертер больше не нужен, его экземпляр по хендлу уничтожается функцией destroy_converter. Для конвертации строки в число используется функция convert_value, куда так же передается хендл.
Интерфейс у "библиотеки" чисто сишный, хотя внутри используется махровый C++11. В принципе, если разделить код на h-файл и .cpp-файлы, то можно было бы создать библиотеку, которую можно было бы линковать как с C-шному коду, так и использовать из других языков, имеющий С-шный FFI (например, Ruby, Python, Erlang и т.д.). Но сейчас все собрано в одном единственном файле для того, чтобы код примера можно было затолкнуть в генерируемую Doxygen-ом документацию.
Полный код примера с достаточным количеством пояснительных комментариев приведен ниже. Пока же добавлю еще несколько слов.
В коде используется несколько новых фич версии 5.5.9, которых раньше не было и которые несколько упрощают использование SObjectizer. Во-первых, это возможность отсылать в качестве сообщений агентов объекты любых типов, наследование от so_5::rt::message_t уже не обязательно. Так, в примере строка для конвертации отсылается просто в виде std::string и агент-конвертер подписывается именно на получение std::string-а из почтового ящика.
Во-вторых, в 5.5.9 добавлены более простые функции для выполнения синхронных запросов к агентам. Так, в коде есть вызов so_5::request_value() вместо цепочки из get_one().waif_forever().make_sync_get(). Длинные цепочки вызовов появились во времена, когда нужно было поддерживать VC++ные компиляторы без variadic templates. Теперь таких ограничений нет и можно создавать более удобное API.
В-третьих, используется новый класс wrapped_env_t, который является специальной оберткой над экземпляром SO Environment и предназначен для предоставления возможности использования SO-5 в привычной для многих C++ников манере, вроде:
int main() {
so_5::wrapped_env_t env; // Запуск пустого Environment.
... // Какой-то код.
env.introduce_coop(...); // Наполнение Environment.
... // Еще какой-то код.
env.stop_then_join(); // Останов и ожидание завершения Environment.
... // Еще какой-то код.
return 0;
}
В примере каждый вызов create_converter как раз приводит к созданию нового объекта wrapped_env_t. А указатель на wrapped_env_t и является хендлом конвертера.
Ну а вот, собственно, и весь код примера с комментариями (в варианте C++14).
И у меня есть просьба к заинтересовавшимся читателям: если вам кажется, что что-то выглядит коряво и сложно, и вам хотелось бы видеть что-то другое, то не сочтите за труд и укажите, что именно. Это сильно поможет сделать SO-5 лучше и ближе к потенциальным пользователям.
/*
* Очень простой пример, демонстрирующий использование SO Environment
* внутри библиотеки, выставляющей наружу plain-old-C интерфейс.
*/
#include <iostream>
#include <string>
#include <stdexcept>
#include <sstream>
// Подключаем все необходимое из SObjectizer.
#include <so_5/all.hpp>
//
// API библиотеки.
// Функции create_converter и convert_value возвращают 0 в случае
// успешного завершения или отличный от 0 код ошибки в случае неудачи.
//
// Псевдо-тип для хендла конкретного экземпляра конвертера.
struct converter;
// Создание экземпляра конвертера.
// Если создание завершается успешно, аргумент handle_receiver
// получает хендл созданного экземпляра.
extern "C" int create_converter( converter ** handle_receiver );
// Конвертация значения.
// Если конвертация завершается успешно, аргумент receiver
// получает полученное из source_value значение.
extern "C" int convert_value(
converter * handle,
const char * source_value,
int * receiver );
// Уничтожение экземпляра конвертера по его хэндлу.
extern "C" void destroy_converter( converter * handle );
//
// Использование библиотеки.
//
// Демонстрационная функция, которая получает вектор значений для
// конвертации, пытается конвертировать каждое из них и возвращает
// вектор описаний результатов конвертации (успешно ли конвертировано
// значение или нет).
std::vector< std::string >
make_conversion(
const std::vector< std::string > & values )
{
// Создаем экземпляр конвертера и сохраняем его хендл.
converter * handle = nullptr;
const int rc1 = create_converter( &handle );
if( rc1 )
// При ошибке создания просто выбрасываем исключение и завершаем работу.
throw std::runtime_error( "converter creation error: " +
std::to_string( rc1 ) );
// Для того, чтобы автоматически удалить экземпляр конвертера
// используется std::unique_ptr. Разрушение converter_deleter при
// выходе из make_conversion приведет к автоматическому вызову
// библиотечной функции destroy_converter.
std::unique_ptr< converter, void (*)(converter *) > converter_deleter(
handle, destroy_converter );
std::vector< std::string > result;
// Выполняем конверсию всех входных значений и накапливаем результирующие
// описания в переменной result.
for( const auto & s : values )
{
// Выполняем конверсию посредством вызова библиотечной функции.
int int_value = 0;
const int rc2 = convert_value( handle, s.c_str(), &int_value );
// Создаем описание в зависимости от результата.
if( rc2 )
result.push_back( "error=" + std::to_string( rc2 ) );
else
result.push_back( "success=" + std::to_string( int_value ) );
}
return result;
}
// Основная функция, которая запускает демонстарацию.
// Внутри создается две исходных последовательности, для обработки
// которых выполняется два асинхронных вызова make_conversion.
// Т.е. каждая последовательность обрабатывается независимо и,
// возможно, обработка осуществляется параллельно.
void demo()
{
// Две исходные последовательности.
std::vector< std::string > seq1{ "1", "2", "three", "4" };
std::vector< std::string > seq2{ "11", "12", "thirteen", "14" };
// Запуск асинхронной обработки двух последовательностей.
// Возвращаются объекты std::future из которых затем будет
// извлекаться результирующее значение.
auto f1 = std::async( make_conversion, std::cref(seq1) );
auto f2 = std::async( make_conversion, std::cref(seq2) );
// Вспомогательная вложенная функция для печати результатов.
auto printer = []( auto * name, const auto & result ) {
std::cout << name << ": " << std::flush;
for( const auto & i : result )
std::cout << i << ",";
std::cout << std::endl;
};
// Получение и печать результатов.
// Если результаты еще не готовы, то произойдет ожидание внутри get().
printer( "First sequence", f1.get() );
printer( "Second sequence", f2.get() );
}
// Главная фукнция, в которой запускается демонстрация и отлавливаются ошибки.
int main()
{
try
{
demo();
}
catch( const std::exception & x )
{
std::cerr << "Exception: " << x.what() << std::endl;
return 2;
}
return 0;
}
//
// Реализация библиотеки.
//
extern "C" int create_converter( converter ** handle_receiver )
{
try
{
// Каждому экземпляру конвертера будет соответствовать свой экземпляр
// SO Environment.
// Этот экземпляр должен быть создан динамически.
// Временно он хранится в unique_ptr для обеспечения exception safety.
std::unique_ptr< so_5::wrapped_env_t > env{ new so_5::wrapped_env_t{} };
// Для работы конвертера в SO Environment нужно создать одну
// кооперацию с единственным агентом внутри.
env->environment().introduce_coop( [&]( so_5::rt::coop_t & coop ) {
// Для конвертации нужно отослать сообщение в почтовый ящик,
// откуда его заберет агент-конвертер.
// Это будет именованный почтовый ящик с жестко зафиксированным именем,
// что позволит обращаться к нему из функции convert_value.
auto mbox = coop.environment().create_local_mbox( "converter" );
// Агент-конвертер.
// Обрабатывает единственное сообщение, поэтому нет смысла
// описывать тип агента в виде C++ класса. Достаточно простого
// ad-hoc агента с единственным событием.
// Агент обрабатывает сообщение типа std::string, отосланное на почтовый
// ящик с именем "converter". В результате его обработки либо
// возвращается целое число, извлеченное из исходной строки, либо же
// порождается исключение.
coop.define_agent().event( mbox, []( const std::string & v ) -> int {
std::istringstream s{ v };
int result;
s >> result;
if( s.fail() )
throw std::invalid_argument( "unable to convert to int: '" + v + "'" );
return result;
} );
} );
// Экземпляр конвертера полностью сформирован, теперь можно возвратить
// указатель на SO Environment в качестве хендла.
*handle_receiver = reinterpret_cast< converter * >( env.release() );
}
catch( const std::exception & )
{
return -1;
}
return 0;
}
extern "C" int convert_value(
converter * handle,
const char * source_value,
int * receiver )
{
try
{
// Мы знаем, что хендл -- это указатель на SO Environment.
auto env = reinterpret_cast< so_5::wrapped_env_t * >( handle );
// Для получения результата конвертации используем синхронный запрос.
// Т.е. текущая нить будет приостановлена до тех пор, пока сообщение
// типа std::string, отосланное в почтовый ящик с именем "converter",
// не будет обработано (или проигнорировано).
*receiver = so_5::request_value< int, std::string >(
// Получаем ссылку на почтовый ящик.
env->environment().create_local_mbox( "converter" ),
// Ждем без ограничений по времени.
so_5::infinite_wait,
// Содержимое сообщения, которое будет отослано.
source_value );
}
catch( const std::exception & )
{
return -2;
}
return 0;
}
extern "C" void destroy_converter( converter * handle )
{
// Просто удаляем ставший ненужным SO Environment.
// В процессе удаления SO Environment будет остановлен,
// работающий внутри SO Environment агент будет дерегистрирован,
// а все созданные SO Environment ресурсы будут освобождены.
auto env = reinterpret_cast< so_5::wrapped_env_t * >( handle );
delete env;
}
|
Комментариев нет:
Отправить комментарий