понедельник, 5 сентября 2011 г.

echo_server revisited

Наконец-то закончен "канонический вариант" активного объекта - ma::echo::server::session и ma::echo::server::session_manager.

Единственное по-настоящему важное изменение – это введение явных раздельных state machine (КА) для внешней части активного объекта (proxy) и для внутренней части (implementation). Так же явно введены КА для каждого вида internal activities. При этом код стал визуально чище и проще (на днях заглянул в код Qt - бр-р-р).

Удалось обойтись без Boost.MSM. Пока я не вижу, чем могла бы помочь данная библиотека, т.к. налицо несколько взаимосвязанных КА, либо какой-то combinatorial FSM.

P.S. Из-за загруженности на основной работе впопыхах пропустил пару глупых ошибок как в коде/логике, так и в комментариях – поэтому текущая версия уже 0.3.5.

13 комментариев:

niXman комментирует...

Марат здравствуйте!

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

спасибо еще раз!

Marat комментирует...

0.3.x - это то, что я хотел написать еще 2 года назад. После того, как доведу echo_server (все еще хочу применить MSM или же как-то закрепить набор состояний и правила взаимодействия КА, входящих в состав активного объекта), займусь давно обещанным чатом.

Timur R. комментирует...

Здравствуйте Марат.

В примерах ASIO демонстрируется использование так называемых "stackless coroutines". Как я понял, сделано это автором как более короткая и наглядная реализация!? И совсем не об оптимизации!? Или я ошибаюсь?

Где-то в комментариях niXman-а проскальзывало утверждение того, что такая реализация "stackless coroutines" позволяют повысить производительность, но подтверждение сему я не нашел - ни в приведенном примере HTTP Server4, ни в комментариях самого Christopher Kohlhoff на его блоге.

Буду Вам признателен, если Вы мне поможете прояснить..

Marat комментирует...

stackless coroutines, представленные автором Asio, являются всего лишь "синтаксическим сахаром". Они позволяют не разделять поток управления (control flow) на несколько функций (или функций-членов) а заменить их костылем из макросов и switch.

При этом одна stackless coroutine позволяет визуально упростить один control flow. В echo_server для каждого ma::echo::server::session м/б сразу несколько control flow, они могут объединяться (join) и "размножаться" (fork). Даже использование нескольких stackless coroutine в этом случае не поможет, так как вся сложность заключается в правильной реакции ma::echo::server::session на очередное воздействие (команда от ma::echo::server::session_manager, окончание ввода/вывода) в зависимости от текущего состояния объекта.

Timur R. комментирует...

Марат, спасибо за ответ.

Сейчас я внимательно изучаю различные варианты реализации сервера с использованием ASIO и примеры, написанные Вами мне очень здорово помогают разобраться в теме! За что Вам огромное спасибо!

Marat комментирует...

Я бы рекомендовал начинать с тотального чтения документации и разбора примеров. Особо любопытные (в плане реализации) части Asio можно смотреть в исходниках прямо по ходу чтения.
asio samples раскрывают некоторые детали, но далеко не всю картину.
Я, кстати, начинал с "Network programming for Windows NT" (бумажная, на русском), своего эхо-сервера на IOCP и Йона Снейдера ("Эффективное программирование TCP/IP").
После этого исходники Asio (и ACE) уже не казались сложными.

Timur R. комментирует...

Большинство примеров я уже разобрал (кстати после asio samples они читаются на одном духу), а вот документацию всю, пока еще не осилил.

Марат, а как вы относитесь к идее попробовать выделять память, как для объектов сессий, так и для каждой асинхронной операции (custom memory allocation) - память из memory pool? При этом, чтобы исключить синхронизацию между потоками, создать для каждого потока свой memory pool. Таким образом, наибольший по размеру memory pool будет у потока(ов) session manager, который будет создавать и удалять сессии..

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

Timur R. комментирует...

Тогда можно было-бы вместо shared_ptr использовать intrusive_ptr выделяющие память в memory pool-e

Marat комментирует...

выделять память, как для объектов сессий, так и для каждой асинхронной операции (custom memory allocation) - память из memory pool?

Для сессий - можно, но будет несколько сложнее.

Для обратных вызовов (completion handler), благодаря custom memory allocation, достаточно "статического куска памяти" в составе класса session.

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

Не забываем про "умные указатели" - память может особождаться не тем потоком, что произвел ее выделение. boost::shared_ptr с custom allocator на базе пула не спасет от необходимости синхронизировать доступ к этому пулу. Отказаться от "умных указателей" невозможно, если мы хотим обеспечить возможность прервать работу сервера в любой момент простым остановом всех экземпляров io_service - asio::io_service::stop(). Такое бывает необходимо, например, в случае непредвиденного исключения/ошибки.

Тогда можно было-бы вместо shared_ptr использовать intrusive_ptr выделяющие память в memory pool-e

Для ma::echo::server::session_manager нужен именно shared_ptr, т.к. session_manager нуждается в weak_ptr из-за разделения логики сервера по разным экземплярам io_service. К тому же безопаснее (в плане ошибок программирования) использовать готовые boost(std)::shared_ptr, чем писать свой atomic counter (Boost.Atomic еще не всем доступен).

Timur R. комментирует...

Для обратных вызовов (completion handler), благодаря custom memory allocation, достаточно "статического куска памяти" в составе класса session.

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

Не забываем про "умные указатели" - память может особождаться не тем потоком, что произвел ее выделение.

Здесь мне кажется можно сделать финт - shared_ptr будет указывать не на сам удаляемый объект, а на объект обертку, в деструкторе которого мы можем запостить (io_service.post) задачу по удалению самого объекта на нужный поток.. Вот, что-то такое)

На сколько я понимаю выше приведенный финт поможет и с этим: возможность прервать работу сервера в любой момент простым остановом всех экземпляров io_service - asio::io_service::stop()

Для ma::echo::server::session_manager нужен именно shared_ptr, т.к. session_manager нуждается в weak_ptr из-за разделения логики сервера по разным экземплярам io_service.

Здесь http://lists.boost.org/Archives/boost/2002/12/40920.php идет вроде обсуждение "Construct weak_ptr from intrusive_ptr".

К тому же безопаснее (в плане ошибок программирования) использовать готовые boost(std)::shared_ptr, чем писать свой atomic counter (Boost.Atomic еще не всем доступен)

Согласен с Вами! Но если это даст выигрыш в производительности, почему бы и не попробовать..

Marat комментирует...

Здесь мне кажется можно сделать финт - shared_ptr будет указывать не на сам удаляемый объект, а на объект обертку, в деструкторе которого мы можем запостить (io_service.post) задачу по удалению самого объекта на нужный поток.. Вот, что-то такое)

На сколько я понимаю выше приведенный финт поможет и с этим: возможность прервать работу сервера в любой момент простым остановом всех экземпляров io_service - asio::io_service::stop()


Ох не знаю. В любом случае при таком останове деструкторы (а с ними и аллокаторы) будут вызываться не в том потоке, для которого был создан pool-based аллокатор.

Marat комментирует...

Согласен с Вами! Но если это даст выигрыш в производительности, почему бы и не попробовать..

Да меня и за то, что уже написано, могут "съесть" за чрезмерную сложность.

Marat комментирует...

В любом случае при таком останове деструкторы (а с ними и аллокаторы) будут вызываться не в том потоке, для которого был создан pool-based аллокатор.

Если с pool-based аллокатор будет "заключен контракт", гарантирующий доступ к нему не из одного единственного потока, а гарантирующий отсутствие конкурентного доступа - тогда вариант

"shared_ptr будет указывать не на сам удаляемый объект, а на объект обертку, в деструкторе которого мы можем запостить (io_service.post) задачу по удалению самого объекта на нужный поток"

вполне реален.

Только надо помнить, что io_service::post и strand::post довольно тяжелые даже в сравнении со стандартным аллокатором (в смысле не тяжелее, но сравнимы, например, как 10 вызовов по времени == 1)