Pull to refresh

Comments 42

Наконец-то! Автору — огромный респект.
Пару лет назад я пробовал донести коллегам эту мысль — использование сопрограмм и переключения контекста — в асинхронном программировании, но многие не поняли, чем такой подход лучше прямого «чисто асинхронного» подхода.
А выгода в том, что если наш алгоритм обработки сам должен сходить в кучу мест по сети (и тоже, естественно, асинхронно) и по окончании аггрегировать результат, то алгоритм распадается на кучу никак программно не связанных кусков, связанных доморощенным объектом-контекстом, поддерживать которые — адъ. При «сопрограммном» подходе алгоритм выглядит единым последовательным действием без объекта-контекста (все нужное — в локальных данных), — удобно сопровождать. Вся асинхронность скрыта внутри обращений к источникам данных.
На реинкарнацию сопрограмм меня натолкнул Lua с его концепцией thread'ов (которые в Lua — сопрограммы).
Кстати, boost:asio мне не очень нравится именно потому, что он подталкивает к использованию «традиционного» асинхронного подхода, когда алгоритм распадается на кучу несвязанных обработчиков. Но это ИМХО
Рад, что статья понравилась. На самом деле, сопрограммы «переоткрывали» многие, поэтому ничего нового по сути я не написал. Но инертность сознания, конечно, делает свое. Поэтому я попытался явно подчеркнуть преимущество подхода путем сравнения кода. Ну и раскрыть механизмы реализации. Всегда стремился к простоте, и при асинхронности сопрограммы, безусловно, играют яркими красками.
Да, очень похоже. Там, насколько я понял, используются сопрограммы, которые называются fibers:
What makes this possible is D's support for so called fibers (also often called co-routines). Fibers behave a lot like threads, just that they are are actually all running in the same thread. As soon as a running fiber calls a special yield() function, it returns control to the function that started the fiber. The fiber can then later be resumed at exactly the position and with the same state it had when it called yield(). This way fibers can be multiplexed together, running quasi-parallel and using each threads capacity as much as possible.
Да, именно так. Все функции ввода/вывода из предоставляемых модулей выполняют yield «под капотом», но API выглядит как блокирующий. Всё разруливается одним из движков событий на выбор (libevent, libev, win32). Получаются очень простые и быстрые веб-приложения.
перестал читать после того, как увидел что такое Buffer и Handler. хороший пример того, как из С++ можно запросто сделать 'Си с классами'.
думаю, не смысла спрашивать, для чего это нужно было, после: «Здесь я использовал синглтон для io_service, чтобы не передавать его каждый раз в сокет явно во входных параметрах.»
1. А что не так с Buffer и Handler?
2. Там было еще продолжение: «И откуда пользователю знать, что там должен быть какой-то io_service?». Поясню смысл. boost.asio использует io_service для различных сетевых сущностей. В целом, это дело на совести boost.asio. Но для меня, как пользователя интерфейса, не хочется знать про то, что спрятано под капотом движка. Т.к. мы решаем вполне конкретную задачу, то для нее вполне ожидаемо использовать упрощения. В данном случае, упрощение, связанное с введением синглтона, ни в коей мере не умоляет саму идею про асинхронность.
> 1. А что не так с Buffer и Handler?
зачем намеренно ограничивать используемые типы? я, к примеру, вместо 'std::string' хочу использовать мой 'sometype', который, по сути, со 'std::string' не имеет ничего общего, кроме концепта. сейчас же, при использовании вашего кода, у меня не будет такой возможности.
второе — зачем лямбды кастовать в 'std::function<void()>'? какой в этом смысл?

> Там было еще продолжение: «И откуда пользователю знать, что там должен быть какой-то io_service?»
думаете, пользователю asio, действительно не нужно знать про io_service, как будто это что-то ненужное? — это ядро asio. первое что должен знать пользователь asio, это именно io_service. более того, открою вам тайну — проактор реализуется именно io_service`ом.

> упрощение, связанное с введением синглтона
как вы собираетесь обрабатывать исключения, выбрасываемые из io_service::run()?
зачем намеренно ограничивать используемые типы? я, к примеру, вместо 'std::string' хочу использовать мой 'sometype', который, по сути, со 'std::string' не имеет ничего общего, кроме концепта. сейчас же, при использовании вашего кода, у меня не будет такой возможности. второе — зачем лямбды кастовать в 'std::function<void()>'? какой в этом смысл?
По-моему, очевидно — чтобы не писать шаблоны. Для целей статьи этого более, чем достаточно. Более того, не существует «правильного» варианта написания интерфейса, всегда будут недовольные. Ну и, повторюсь, что для сути статьи это абсолютно безразлично.

думаете, пользователю asio, действительно не нужно знать про io_service, как будто это что-то ненужное? — это ядро asio. первое что должен знать пользователь asio, это именно io_service. более того, открою вам тайну — проактор реализуется именно io_service`ом.
После обертки у нас растворились пользователи asio и появились пользователи Socket, Acceptor, go… И я тоже открою тайну: проактор — это теперь (в статье) реализация, а не интерфейс.

как вы собираетесь обрабатывать исключения, выбрасываемые из io_service::run()?
Как-то так:
typedef std::function<void ()> Handler;

void go(Handler handler)
{
    LOG("sync::go");
    std::thread([handler] {
        try
        {
            LOG("new thread had been created");
            handler();
            LOG("thread was ended successfully");
        }
        catch (std::exception& e)
        {
            LOG("thread was ended with error: " << e.what());
        }
    }).detach();
}

несогласен по каждому ответу. не вижу смысла писать об этом.
за труд — похвала, разумеется. но я так и не понял, о чем статья, для чего статья, что конкретно она объясняет, etc…
Вступлюсь за автора. Статья интересно написана. Думаю, что код не для продакшн. А для статьи он хорошо подходит. Для беглого просмотра именно такой код и должен быть: максимально упрощен, типы максимально конкретизированы. Легче вникать. На исключения можно было вообще забить, кода и так очень много, не все до конца доберутся.
Можно было бы, конечно, и на исключения забить, однако там есть несколько нюансов. Поэтому решил, что лучше с ними. И да, цель была не в нагромождении, а в упрощении.
Спасибо, за статью. А почему не захотели Boost.Coroutine использовать?
Ну, автор мог бы не реализовывать свою корутину на базе Boost.Context, а заюзать Boost.Coroutine, которая как раз сама все это делает и на базе той же Boost.Context.
К счастью, есть boost.context, которая и реализует все, что необходимо для поддержки конкретной платформы. Написано все на ассемблере, в лучших традициях. Можно, конечно, использовать boost.coroutine, но зачем, когда есть boost.context? Больше ада и угара!
Предполагаю, что автору был необходим ад и угар :)
предполагаю, автор не читал доку последних нескольких версий boost, а только к версии 1.50, ибо в последних версиях, asio искаропки поддерживает сопрограммы.
Ваше предположение ложно. См. «Небольшая ремарка», плюс небольшой комментарий.
предполагаю, автор не читал доку последних нескольких версий boost, а только к версии 1.50
Вот именно это предположение ложно (т.е. предположение о том, что автор не читал доку).
т.е. выше вы пишете о том, что делаете то что делаете для того, чтоб упростить. так зачем вы усложняете, рукоблудя код с короутинами?
У меня упрощение не является единственной целью. Помимо нее есть еще цель показать, что спрятано под капотом сопрограмм и как они стыкуются с асинхронной частью. И я не считаю приведенный код усложненным, он простой настолько, насколько это вообще возможно. Если вы считаете, что это можно сделать еще проще, тогда напишите как. Я думаю, всем это будет интересно.
Ну, во-первых, хотелось самому пощупать, как оно, реализовать сопрограммы. Во-вторых, хотелось большего контроля над исполнением. В-третьих, хотелось, чтобы читатель примерно понимал, что там под капотом у сопрограмм и как все это дело взаимодействует с асинхронностью. Не вижу причин не использовать в своих проектах уже готовые boost-объекты. Ну и да, для ада и угара.
Главное, поймите меня правильно — no offense. Я лишь ответил на вопрос vScherba, ответ на который был у вас в статье.
> хотелось, чтобы читатель примерно понимал, что там под капотом у сопрограмм и как все это дело взаимодействует с асинхронностью.
выше, вы неоднократно пишите о том, что все это вы сделали для упрощения. но, повторюсь, рукоблудные короутины — не упрощение. могли просто не упоминать о них.
опять же: «чтобы читатель примерно понимал, что там под капотом» — так зачем тогда прятать использование io_service? и, раз уж io_service — ядро всего этого дала — почему про него, в статье почти ничего не говориться?
ядро всего этого *дела
Я видел это в статье, но счел за шутку. Мне показалось, что были дополнительные причины, по которым coroutine не удалось прикрутить.
Отличная статья!
Я тоже эксперементирую с использованием корутин, но уже встроенных в boost::asio stackfull-корутин. Кстати, там у меня получилось определить свой контекст, который не испльзует strand-ы, а только напрямую io_service. Получается весьма шустро, да — по прикидкам около 5 гигабит в секунду на одно ядро (пока негде проверить). Только я стараюсь делать ещё более эффективно, в частности избегаю аллокаций.
В контексте этого, хотелось бы узнать более подробные цифры — какой входящий/исходящий трафик обслуживал Ваш сервер на этих 70-90% ядра?
Простые запросы типа GET /hello HTTP/1.1
Т.е. клиент работал так:
1. Открывается соединение.
2. Посылается запрос.
3. Дожидается ответ, валидируется.
4. Закрывается соединение.
Понятно, что это все тоже делалось асинхронно, чтобы нагружать до 30K RPS.
Мне кажется, что вы создали слишком много инфраструктуры, а потом начали с ней бороться. Для того, чтобы сделать такой http-server, вам достаточно всего двух обработчиков: на создание сокета и на прием сообщеий. Все это можно уложить в 100 строк.
А по-моему цель автора была познакомить читателей с концептом, расширить кругозор так сказать, за что ему и спасибо. А то, что http-сервер можно реализовать как-то по другому — то ни кто и не спорит :-)
Просмотрел пока все картинки, записал статью в закладки.

Спасибо!
В случае асинхронного вызова способ существует ровно один: передача ошибки через обработчик

Я при работе с асинхронными вызовами использую такой подход, упрощающий (как мне кажется) работу с исключениями в дополнительных потоках: если в методе, вызываемом из неосновного потока, возникает исключение, я в catch секции куда-то запоминаю объект исключения, а потом в момент «возврата результата в основной поток» (любое место, где основной поток синхронизируется с дополнительным) я перевозбуждаю исключение в основном потоке, чтобы оно могло быть обработано стандартными средствами.
Программирую я на Delphi, потому, возможно, использую немного иные термины, чем в статье. Могу пояснить на примере, если требуется доп. пояснение.
Не совсем понятно, как это применимо для асинхронного программирования. Думаю, пример был бы полезен для иллюстрации метода.
>> Теорема. Любую асинхронную задачу можно решить с помощью сопрограмм.
Есть одна важная проблема которую решать с помощью сопрограмм сложно и не удобно а на асинхронном подходе «бесплатно» — обработка таймаутов.
В перечисленных примерах HTTP сервера обработка таймаутов скромно пропущена. Но это чуть ли не главная проблема для реальных серверов — быстро уметь разруливать кривых клиентов или проблемы со связью. 30К запросов в секунду-минуту это конечно хорошо в идеальных условиях, а что если 30к подключений на вашем сервере начнут играть «в дурачка» и будут слать по 1му байту в минуту. Для остальных пользователей сервер скорей всего ляжет, кол-во доступных портов же не резиновое.
Стандартные задачи при имплементации протокола — посчитать таймаут между подключением и первым запросом пользователя. Не первым полученным байтом а именно полным запросом. Или заимплементить ограничение на кол-во запросов в минуту сильно нагружающих сервер. На boost::asio это просто шедулинг таймера одной строкой и обработка еще одного калбека, а как в сопрограммах то это сделать?
В resume() встроить код, закрывающий сокет и сопрограмму, если таймаут исчерпан. Средствами boost::asio вызывать resume() по истечении таймаута (не уверен, что нужно это и что гонки не породит). Можно ли так?
Можно сделать так: в сопрограмме устанавливать флажок по таймауту, затем по resume его проверять, и если выставлен, то кидать исключение, которое при этом можно еще и при желании обработать. Так что серьезных проблем я не вижу.
А какой инструментарий/интерфейс будет у сопрограммы на таймауты, напрямую дергать io_service таймеры или что-то свое? Т.е. надо же как-то отмечать — тут таймер начался, тут закончился, тут вообще отключаем его, тут несколько таймеров считаются паралельно и так далее.
Например нам надо посчитать keep-alive timeout из HTTP 1.1, это когда вообще ничего не приходит ~5 секунд.
Ну или грубо если клиент что-то начал слать, то максимум собирать пакет 15 секунд, если не собрался — отключаем.

Просто на моем опыте в линейном коде — блокирующемся или на сопрограммах — таймеры считать напряжно, пропадает весь смысл линейности так как таймеры по сути своей асинхронны — они сработать грубо могут в любом месте где происходит блокировка/resume.
А если делать гибридный подход (io_service-таймеры но остальной код на сопрограммах) то как-бе пропадают плюсы сопрограмм — человеку придется погружатся в пробематику обоих подходов.
Спасибо за статью!

В проекте Wt используется asio. Сопрограммы, к сожалению, не используются. Было бы хорошо использовать сопрограммы в коде, работающем с БД и загружающем данные через HTTP из других источников. Работа с БД вообще синхронна.

Мне кажется, здесь переменная buffer связывается зря.

Кстати, у меня не получилось собрать программу:

$ g++ -std=c++11 *.cpp -o server
In file included from async.cpp:6:0:
sync.h:9:16: error: ‘namespace sync { }’ redeclared as different kind of symbol
In file included from /usr/include/boost/config/stdlib/libstdcpp3.hpp:77:0,
                 from /usr/include/boost/config.hpp:44,
                 from /usr/include/boost/asio/detail/config.hpp:26,
                 from /usr/include/boost/asio/async_result.hpp:18,
                 from /usr/include/boost/asio.hpp:20,
                 from async.h:5,
                 from async.cpp:5:
/usr/include/unistd.h:986:13: error: previous declaration of ‘void sync()’
async.cpp: In function ‘void async::dispatch(int)’:
async.cpp:31:9: error: ‘sync’ is not a class, namespace, or enumeration

Сейчас должно собираться без проблем. Попробуйте
Собралось командой g++ -std=c++11 *.cpp -o server -lboost_system -lboost_context
Работает
Sign up to leave a comment.

Articles