Ответ 1
Фонд
Давайте начнем с упрощенного примера и рассмотрим соответствующие фрагменты Boost.Asio:
void handle_async_receive(...) { ... }
void print() { ... }
...
boost::asio::io_service io_service;
boost::asio::ip::tcp::socket socket(io_service);
...
io_service.post(&print); // 1
socket.connect(endpoint); // 2
socket.async_receive(buffer, &handle_async_receive); // 3
io_service.post(&print); // 4
io_service.run(); // 5
Что такое обработчик?
Обработчик - это не что иное, как обратный вызов. В примере кода имеется 3 обработчика:
- Обработчик
print
(1). - Обработчик
handle_async_receive
(3). - Обработчик
print
(4).
Несмотря на то, что одна и та же функция print()
используется дважды, считается, что каждое использование создает свой собственный однозначно идентифицируемый обработчик. Обработчики могут иметь множество форм и размеров, начиная от базовых функций, таких как выше, до более сложных конструкций, таких как функторы, созданные из boost::bind()
и lambdas. Независимо от сложности, обработчик по-прежнему остается не чем иным, как обратным вызовом.
Что такое работа?
Работа - это некоторая обработка, которую Boost.Asio запрашивал от имени кода приложения. Иногда Boost.Asio может начать часть работы, как только об этом расскажут, и в других случаях она может подождать, чтобы выполнить работу в более поздний момент времени. После того, как он завершит работу, Boost.Asio сообщит об этом приложению, вызвав предоставленный обработчик.
Boost.Asio гарантирует, что обработчики будут работать только в потоке, который в настоящее время вызывает run()
, run_one()
, poll()
или poll_one()
. Это потоки, которые будут работать и обработчики вызовов. Поэтому в приведенном выше примере print()
не вызывается, когда он отправляется в io_service
(1). Вместо этого он добавляется в io_service
и будет вызываться в более поздний момент времени. В этом случае он находится в пределах io_service.run()
(5).
Что такое асинхронные операции?
А <а href= "http://www.boost.org/doc/libs/1_56_0/doc/html/boost_asio/reference/asynchronous_operations.html" rel= "noreferrer" > асинхронная операция создает работу и Boost.Asio будет вызывать обработчик, чтобы сообщить приложение, когда работа завершена. Асинхронные операции создаются путем вызова функции с именем с префиксом async_
. Эти функции также известны как инициирующие функции.
Асинхронные операции и могут быть разложены на три уникальных шага:
- Инициирование или информирование связанного с ним
io_service
, которое должно быть выполнено. Операцияasync_receive
(3) сообщаетio_service
, что ей потребуется асинхронно считывать данные из сокета, а затемasync_receive
немедленно возвращается. - Выполнение фактической работы. В этом случае, когда
socket
получает данные, байты будут считаны и скопированы вbuffer
. Фактическая работа будет выполнена либо:- Инициирующая функция (3), если Boost.Asio может определить, что она не будет блокироваться.
- Когда приложение явно запускает
io_service
(5).
- Вызов
handle_async_receive
ReadHandler. Еще раз, обработчики вызываются только в потоках, выполняющихio_service
. Таким образом, независимо от того, когда работа выполнена (3 или 5), гарантируется, чтоhandle_async_receive()
будет вызван только вio_service.run()
(5).
Разделение во времени и пространстве между этими тремя этапами называется инверсией потока управления. Это одна из сложностей, затрудняющих асинхронное программирование. Однако есть методы, которые могут помочь смягчить это, например, используя сопрограммы.
Что делает io_service.run()
Do?
Когда поток вызывает io_service.run()
, из этого потока будут вызываться работа и обработчики. В приведенном выше примере io_service.run()
(5) будет заблокирован до:
- Он вызывается и возвращается из обоих обработчиков
print
, операция получения завершается с успехом или неудачей, и его обработчикhandle_async_receive
был вызван и возвращен. -
io_service
явно остановлен черезio_service::stop()
. - Исключение выбрано из обработчика.
Один потенциальный псевдо-иш-поток можно описать следующим образом:
create io_service create socket add print handler to io_service (1) wait for socket to connect (2) add an asynchronous read work request to the io_service (3) add print handler to io_service (4) run the io_service (5) is there work or handlers? yes, there is 1 work and 2 handlers does socket have data? no, do nothing run print handler (1) is there work or handlers? yes, there is 1 work and 1 handler does socket have data? no, do nothing run print handler (4) is there work or handlers? yes, there is 1 work does socket have data? no, continue waiting -- socket receives data -- socket has data, read it into buffer add handle_async_receive handler to io_service is there work or handlers? yes, there is 1 handler run handle_async_receive handler (3) is there work or handlers? no, set io_service as stopped and return
Обратите внимание, что когда прочитанный закончен, он добавляет другой обработчик к io_service
. Эта тонкая деталь является важной особенностью асинхронного программирования. Это позволяет привязывать обработчиков вместе. Например, если handle_async_receive
не получил все ожидаемые данные, тогда его реализация могла бы отправить другую асинхронную операцию чтения, в результате получив io_service
больше работы и, таким образом, не возвращаясь из io_service.run()
.
Обратите внимание, что при завершении работы io_service
приложение должно reset()
io_service
перед повторным запуском.
Пример и пример кода 3a
Теперь рассмотрим две части кода, на которые ссылается вопрос.
Код вопроса
socket->async_receive
добавляет работу к io_service
. Таким образом, io_service->run()
будет блокироваться до тех пор, пока операция чтения не завершится с успехом или ошибкой, а ClientReceiveEvent
либо закончит выполнение, либо исключит исключение.
Пример 3a Код
В надежде на то, что это будет легче понять, вот небольшой аннотированный пример 3a:
void CalculateFib(std::size_t n);
int main()
{
boost::asio::io_service io_service;
boost::optional<boost::asio::io_service::work> work = // '. 1
boost::in_place(boost::ref(io_service)); // .'
boost::thread_group worker_threads; // -.
for(int x = 0; x < 2; ++x) // :
{ // '.
worker_threads.create_thread( // :- 2
boost::bind(&boost::asio::io_service::run, &io_service) // .'
); // :
} // -'
io_service.post(boost::bind(CalculateFib, 3)); // '.
io_service.post(boost::bind(CalculateFib, 4)); // :- 3
io_service.post(boost::bind(CalculateFib, 5)); // .'
work = boost::none; // 4
worker_threads.join_all(); // 5
}
На высоком уровне программа создаст 2 потока, которые будут обрабатывать цикл событий io_service
(2). Это приводит к простому пулу потоков, который будет вычислять числа Фибоначчи (3).
Единственное важное различие между кодом вопроса и этим кодом состоит в том, что этот код вызывает io_service::run()
(2) до того, как фактическая работа и обработчики добавлены в io_service
(3). Чтобы предотвратить немедленное возвращение io_service::run()
, создается объект io_service::work
(1). Этот объект предотвращает работу io_service
; поэтому io_service::run()
не будет возвращаться в результате отсутствия работы.
Общий поток выглядит следующим образом:
- Создайте и добавьте объект
io_service::work
, добавленный вio_service
. - Создан пул потоков, который вызывает
io_service::run()
. Эти рабочие потоки не возвращаются изio_service
из-за объектаio_service::work
. - Добавьте 3 обработчиков, которые вычисляют числа Фибоначчи в
io_service
и немедленно возвращаются. Рабочие потоки, а не основной поток, могут немедленно запустить эти обработчики. - Удалить объект
io_service::work
. - Дождитесь завершения рабочих потоков. Это произойдет только после завершения всех трех обработчиков, так как
io_service
не имеет обработчиков и не работает.
Код может быть написан по-разному, таким же образом, как и исходный код, где обработчики добавляются в io_service
, а затем обрабатывается цикл событий io_service
. Это устраняет необходимость использования io_service::work
и приводит к следующему коду:
int main()
{
boost::asio::io_service io_service;
io_service.post(boost::bind(CalculateFib, 3)); // '.
io_service.post(boost::bind(CalculateFib, 4)); // :- 3
io_service.post(boost::bind(CalculateFib, 5)); // .'
boost::thread_group worker_threads; // -.
for(int x = 0; x < 2; ++x) // :
{ // '.
worker_threads.create_thread( // :- 2
boost::bind(&boost::asio::io_service::run, &io_service) // .'
); // :
} // -'
worker_threads.join_all(); // 5
}
Синхронный и асинхронный
Хотя в этом вопросе используется асинхронная операция, он эффективно работает синхронно, поскольку он ожидает завершения асинхронной операции:
socket.async_receive(buffer, handler)
io_service.run();
эквивалентно:
boost::asio::error_code error;
std::size_t bytes_transferred = socket.receive(buffer, 0, error);
handler(error, bytes_transferred);
Как правило, старайтесь избегать смешивания синхронных и асинхронных операций. Часто это может превратить сложную систему в сложную систему. Этот ответ показывает преимущества асинхронного программирования, некоторые из которых также описаны в документации Boost.Asio .