понедельник, 13 июня 2011 г.

Design Journeys with Asio

Предлагаю читателям этого блога ответить на довольно простой вопрос:

зачем в требованиях Asio к функтору-обработчику существует asio_handler_invoke?

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

Updated:
Я неправильно выразил вопрос. Кхм... скажем по-проще: как Вы думаете, зачем автор Asio усложнил дизайн, введя asio_handler_invoke (будем называть это invocation strategy)? Что потребовало от него именно такого похода?

Вопрос отчасти связан с тем, что многие считают Asio слишком сложной библиотекой. Этот вопрос направлен на то, чтобы объяснить, что в Asio нет ничего "случайного". Все имеет свою вескую причину.

Жаль, что на BoostCon 2011 Chris не сделал доклада с заголовком, похожим на заголовок данного сообщения.

Updated:
Когда я только начинал использовать Boost.Asio, наличие invocation strategy меня несколько смутило: что именно в моем MyHandler::operator() относится к invocation strategy?

И вот, когда я снова перечитал документацию Boost.Asio и нашел пункт X, я понял:
  1. какая часть MyHandler::operator() относилась к invocation strategy;
  2. для чего вообще было введено понятие invocation strategy.
Позже я понял, что invocation strategy лучше вынести из MyHandler::operator(), т.е. оставить ее только в соотв. asio_handler_invoke. В противном случае надо быть готовым к рекурсивному использованию invocation strategy. Например, вместо boost::mutex использовать boost::recursive_mutex (см. MA_BOOST_ASIO_HEAVY_STRAND_WRAPPED_HANDLER в asio-samples).

Так вот: ответ (и сопутствующее обсуждение) на вопрос, заданный мной в данном сообщении, дополнительно должен помочь разработчикам, использующим Boost.Asio, понять - какую часть MyHandler::operator() следует выносить в asio_handler_invoke и как выносить данный код - оставляя его же в MyHandler::operator() или нет?

Updated:
Как показало время, этот вопрос оказался либо неинтересным, либо слишком сложным.

Ну что ж, вот правильный ответ: Strands: Use Threads Without Explicit Locking.

Читаем: "if a completion handler goes through a strand, then all intermediate handlers should also go through the same strand. This is needed to ensure thread safe access for any objects that are shared between the caller and the composed operation (in the case of async_read() it's the socket, which the caller can close() to cancel the operation). This is done by having hook functions for all intermediate handlers which forward the calls to the customisable hook associated with the final handler".

Объясняю проще: единственная причина введения asio_handler_invoke - это правильная работа composed operations в свете синхронизации доступа к IoObject. Только такой hook и мог помочь в данном случае. Все остальные места в нем особо не нуждались, так как они так или иначе всегда заканчиваются вызовом Handler::operator() без необходимости повторного доступа к IoObject (могу ошибаться насчет поддержки SSL - еще не разбирался с ней).

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

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

или я не понял подвоха, или что-то еще...
в доке:http://think-async.com/Asio/boost_asio_1_5_3/doc/html/boost_asio/reference/asio_handler_invoke.html
сказано:

Implement asio_handler_invoke for your own handlers to specify a custom invocation strategy.

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

или я не понял подвоха, или что-то еще...

...или я не так задал вопрос. Сейчас подправлю.

Implement asio_handler_invoke for your own handlers to specify a custom invocation strategy.

1. Очень близко, даже "горячо", к тому варианту ответа, что готов предложить я.
2. invocation strategy - вот оно, правильное название - именно strategy а не context. Пойду поменяю комментарии в коде asio-samples.

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

здравствуйте.
скажу Вам по секрету: я ничего не понял %)
...и это правда. недорос еще, видимо)

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

я ничего не понял
Не понял вопроса? Ну это скорее моя вина. Можно подробнее (прямо по тексту)?

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

во первых - я до сих пор не понял(сейчас перечитал, и все равно не понял) память_для_чего_он_выделяет?
предполагаю, что раз уж там есть invoke() то наверное это память для функциональных объектов?)

п.с.
исходники не смотрел.

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

память_для_чего_он_выделяет?
О памяти речь еще не шла (но будет после). Это custom memory allocation. И там разговоров гораздо больше, т.к. это очень важная для production возможность.

Пока я говорю только о вызове обработчика. В Asio эти два вопроса четко разделены.

наверное это память для функциональных объектов?
Отвечу, забегая вперед (custom memory allocation) - да, но не только для них. По документации - для всего временного, что может потребоваться. На практике - в основном, это "обернутые" функторы, содержащие дополнительную информацию, например, для intrusive-списков.

исходники не смотрел
Исходники asio samples или Boost.Asio?
Если последнее, то плоха та документация, что заставляет разработчика смотреть в исходники. Я в них смотрел - но лишь для того, чтобы убедиться в том, что там "все правильно сделано" (достаточно эффективно для использования в production).

Хорошо, что есть кто-то, кто задает вопросы. Потому что, например, я часто не могу выразить довольно простую мысль доступным языком (мне об этом иногда говорят, когда поймут, что я имел в виду).
Вы уже дали близкий ответ. В документации Asio есть пункт, где говорится о том, что есть X и есть Y, который использует X через asio_handler_invoke. Так же там написано, что если вместо X Вы захотите использовать что-то свое, то не забудьте про asio_handler_invoke, иначе при использовании Y "огребете проблем". Я внимательно просмотрел Boost.Asio и понял, что asio_handler_invoke вообще нужен только для Y. Вот этот вывод и будет ответом на поставленный в этом сообщении вопрос. Кроме того, он поможет понять, что нужно писать в своем asio_handler_invoke, как и зачем (когда).

niXman комментирует...
Этот комментарий был удален автором.
niXman комментирует...

Отвечу, забегая вперед (custom memory allocation) - да, но не только для них. По документации - для всего временного, что может потребоваться.
т.е. и для буферов/данных тоже?

п.с.
вообще, у меня сейчас два неясных момента касательно Asio(т.е. тех, что я осознаю):
1. custom memory allocation: что оно конкретно делает и как/чему помогает.
2. strands: это вообще не могу понять... ну не знаю почему. обычно, такое случается если пропущено что-то важное, предшествующее пониманию strand`ов..

это нужно тупо писать код и ставить эксперименты.

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

т.е. и для буферов/данных тоже?

Не совсем так в теории и совсем не так на практике. Отложим это до соотв. темы.

custom memory allocation: что оно конкретно делает и как/чему помогает

Помогает избавиться от выделения памяти в куче при асинхронных операциях. На каждую асинхронную операцию приходится минимум одно выделение памяти (strand-ы добавляют еще одно-два). Если не использовать custom memory allocation, то все это будет за счет кучи (и global operator new). Т.е. о скорости уже можно забыть. Так же для сервера это чревато фрагментацией памяти (теоретически).

strands: это вообще не могу понять

Тут все просто: strand - это оболочка над очередью функторов. При чем, эта очередь привязана к конкретному экземпляру io_service и работает только в паре с ним. Плюсом идет специфика Asio. Например, допускается чтобы функтор, который будет "положен" (post/dispatch) в экземпляр strand, удерживал (например, при помощи shared_ptr) этот самый экземпляр strand. Это весьма важный пунктик.

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

Ну вот.. Наконец-то. Перечитал несколько раз, и до меня дошло! :D

Взял пример из "custom memory allocation example" добавил в него asio_handler_invoke. Даже работает)
http://liveworkspace.org/code/d8f9abec20e6d54bd590d53f53f373a4

Переписываю свои проекты с использованием Custom Memory Allocation.

Спасибо что дали информацию к размышлению.

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

Не очень ясно, исходя из чего определяется объем аллокатора?

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

Не очень ясно, исходя из чего определяется объем аллокатора?

"Опытным путем". В этот объем должны помещаться:
1) все промежуточные функторы, созданные на основе Вашего completion handler (т.е. как минимум, включающие его в себя),
2) элементы IOCP/strand/timer-очередей (где так же используется custom memory allocation).

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

Переписываю свои проекты с использованием Custom Memory Allocation.

Мне недавно "подсказали" - не всегда есть смысл такого перехода.
Все же для большинства проектов Custom Memory Allocation - это усложнение кода и повышение вероятности появления ошибок. Так что нужно "посчитать выигрыш" - он будет тем больше, чем более Ваше приложение зависит от скорости ввода-вывода. Если же большую часть времени Ваше приложение тратит на обработку запросов, а частота ввода-вывода невелика, то и "выигрыш" будет почти незаметн.

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

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

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

и еще не понятно, почему в этом экзампле: http://www.boost.org/doc/libs/1_47_0/doc/html/boost_asio/example/allocation/server.cpp
нет asio_handler_invoke().. %)

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

возник еще такой вопрос:
есть класс-обертка над acceptor::async_accept():

// реализация
template
void async_accept(const F& f, ...) {
acceptor.async_accept(
socket,
f
);
}

// адаптер
void async_accept(handler, ...) {
async_accept(boost::bind(handler, _1, _2, _3)); // вызывает метод выше ^^^
}


вопрос в том, где тут правильней использовать custom_memory_allocation? в реализации, или в адаптере?

спасибо.

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

и еще не понятно, почему в этом экзампле http://www.boost.org/doc/libs/1_47_0/doc/html/boost_asio/example/allocation/server.cpp
нет asio_handler_invoke()..


По-хорошему он там должен быть. И должен быть аналогичен asio_handler_invoke из ma::custom_alloc_handler. Но конкретно для того примера наличие asio_handler_invoke некритично. В общем же случае "правильный" вариант см. в ma::custom_alloc_handler.

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

вопрос в том, где тут правильней использовать custom_memory_allocation? в реализации, или в адаптере?

Честно говоря, не понял, зачем нужна обертка над acceptor::async_accept. Обычно custom_alloc_handler (или его аналог) оставляют для реализации пользователю библиотеки (он-то должен знать, как это лучше реализовать - ему известен размер completion handler и пр.).

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

можете подсказать?

Если в том примере Handler будет являться bound completion handler (bch) (например, intermediate completion handler of composed operation) и будет использоваться explicit strand (io_service::strand) для completion handler, исходного по отношению к bound completion handler, то вызов custom_alloc_handler (Asio всегда выполняет его через asio_handler_invoke) приведет к непосредственному вызову bch, вместо диспетчеризации bch через strand.

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

вот более развернутый пример: http://liveworkspace.org/code/6d422f30a91f4e1875dfd98c2700d39b

вопрос в том, где будет правильней создавать преаллоцированный хендлер? в адаптере, или же функциональный объект созданный bind`ом передавать в реальный метод, и в нем создавать преаллоцированный хендлер?

Marat Abrarov комментирует...
Этот комментарий был удален автором.
niXman комментирует...
Этот комментарий был удален автором.
Marat Abrarov комментирует...

вопрос в том, где будет правильней создавать преаллоцированный хендлер?

В предыдущем (удаленном) комментарии я ошибся.

По-моему, разницы особой нет. Однако, полный размер handler будет известен в "реальном методе" - все же там будет проще подобрать правильный способ реализации custom allocator.

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

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

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

добрый день!

скажите, логичным ли будет использовать два аллокатора на сокет? т.е. один для хэндлеров записи, и второй для хэндлеров чтения?
т.к. в этом примере: http://www.boost.org/doc/libs/1_47_0/doc/html/boost_asio/example/allocation/server.cpp
используется один аллокатор...

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

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

Да. Я так и сделал в ma::echo::server::session.

В примере из документации Asio используется один аллокатор ровно потому, что там на одном и том же сокете не бывает одновременно и чтения и записи. У меня бывает - поэтому я использую 2 аллокатора (read/write) + отдельный аллокатор для таймера.

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

вах!
вах! при использовании custom memory allocation, производительность кода увеличилась почти в 2.5 раза!
раньше, имел 200000-206000 при полностью съеденных 4ех ядрах. сейчас, почти 480000! в 2.5 раза точно!

зы
в это сложно поверить!
зызы
Марат, спасибо Вам за помощь!

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

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

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

Все "спасибо" - заслуга автора Asio.
Все, что я писал про custom memory allocation, есть в документации Boost.Asio.
Все, что использую в asio samples, является лишь логическим выводом (а местами и copy/paste) из примеров и внутренностей Boost.Asio.

Рад был помочь. Надеюсь, что Asio будет чаще (и глубже) обсуждаться и применяться на просторах СНГ.

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

раньше, имел 200000-206000 при полностью съеденных 4ех ядрах. сейчас, почти 480000! в 2.5 раза точно!

Подробности в студию.
Как минимум:
1. среда исполнения (ОС, процессор, RAM),
2. компилятор.

niXman комментирует...
Этот комментарий был удален автором.
niXman комментирует...

Ubuntu-11.04/AMD Phenom 9650/4096Gb/gcc-4.5.2/boost-1.47.0.
банальный запрос-ответ. т.е. интересовала именно скорость создания/вызова функциональных объектов.

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

что-то я не понимаю, почему в примере: http://www.boost.org/doc/libs/1_47_0/doc/html/boost_asio/example/allocation/server.cpp
используется всего один аллокатор для двух функциональных объектов?
см. методы:
session::handle_read()/session::handle_write.

и не поэтому-ли в этом примере не используется asio_handler_invoke() ?

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

что-то я не понимаю, почему в примере: http://www.boost.org/doc/libs/1_47_0/doc/html/boost_asio/example/allocation/server.cpp
используется всего один аллокатор для двух функциональных объектов?


Овечал выше, но повторю. Asio custom memory allocation всегда освобождает handler-related-аллокатор перед вызовом completion handler. Поэтому, если разные операции гарантированно не выполняются одновременно, то для них можно использовать один и тот же аллокатор.

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

ах да.
запутался %)