Fundação
Vamos começar com um exemplo simplificado e examinar as peças Boost.Asio relevantes:
void handle_async_receive(...) { ... }
void print() { ... }
...
boost::asio::io_service io_service;
boost::asio::ip::tcp::socket socket(io_service);
...
io_service.post(&print);
socket.connect(endpoint);
socket.async_receive(buffer, &handle_async_receive);
io_service.post(&print);
io_service.run();
O que é um manipulador ?
Um manipulador nada mais é do que um retorno de chamada. No código de exemplo, existem 3 manipuladores:
- O
print
manipulador (1).
- O
handle_async_receive
manipulador (3).
- O
print
manipulador (4).
Mesmo que a mesma print()
função seja usada duas vezes, cada uso é considerado para criar seu próprio manipulador identificável exclusivamente. Os manipuladores podem ter várias formas e tamanhos, desde funções básicas como as acima até construções mais complexas, como functores gerados de boost::bind()
e lambdas. Independentemente da complexidade, o manipulador ainda permanece nada mais do que um retorno de chamada.
O que é trabalho ?
Trabalho é algum processamento que o Boost.Asio foi solicitado a fazer em nome do código do aplicativo. Às vezes, o Boost.Asio pode iniciar parte do trabalho assim que for informado sobre ele e, outras vezes, pode esperar para fazer o trabalho posteriormente. Depois de concluir o trabalho, Boost.Asio informará o aplicativo chamando o manipulador fornecido .
Boost.Asio garante que manipuladores só será executada dentro de um segmento que está chamando run()
, run_one()
, poll()
, ou poll_one()
. Esses são os threads que farão o trabalho e chamarão os manipuladores . Portanto, no exemplo acima, print()
não é invocado quando é postado no io_service
(1). Em vez disso, ele é adicionado ao io_service
e será chamado posteriormente. Nesse caso, dentro de io_service.run()
(5).
O que são operações assíncronas?
Uma operação assíncrona cria trabalho e Boost.Asio invocará um manipulador para informar o aplicativo quando o trabalho for concluído. As operações assíncronas são criadas chamando uma função que tem um nome com o prefixo async_
. Essas funções também são conhecidas como funções de inicialização .
As operações assíncronas podem ser decompostas em três etapas exclusivas:
- Iniciar, ou informar, o associado
io_service
que funciona precisa ser feito. A async_receive
operação (3) informa io_service
que será necessário ler dados do soquete de forma assíncrona e async_receive
retorna imediatamente.
- Fazendo o trabalho real. Neste caso, ao
socket
receber dados, os bytes serão lidos e copiados para buffer
. O trabalho real será feito em:
- A função de inicialização (3), se Boost.Asio puder determinar que não bloqueará.
- Quando o aplicativo executa explicitamente o
io_service
(5).
- Invocando o
handle_async_receive
ReadHandler . Mais uma vez, os manipuladores são invocados apenas em threads que executam o io_service
. Assim, independentemente de quando o trabalho é feito (3 ou 5), é garantido que handle_async_receive()
só será invocado dentro de io_service.run()
(5).
A separação no tempo e no espaço entre essas três etapas é conhecida como inversão de fluxo de controle. É uma das complexidades que tornam a programação assíncrona difícil. No entanto, existem técnicas que podem ajudar a mitigar isso, como o uso de corrotinas .
O que io_service.run()
faz?
Quando um thread é chamado io_service.run()
, o trabalho e os manipuladores são chamados de dentro desse thread. No exemplo acima, io_service.run()
(5) irá bloquear até:
- Ele foi chamado e retornou de ambos os
print
manipuladores, a operação de recebimento foi concluída com sucesso ou falha e seu handle_async_receive
manipulador foi chamado e retornado.
- O
io_service
é explicitamente parado por meio de io_service::stop()
.
- Uma exceção é lançada de dentro de um manipulador.
Um fluxo potencial psuedo pode ser descrito como o seguinte:
criar io_service
criar soquete
adicionar manipulador de impressão a io_service (1)
aguarde o soquete conectar (2)
adicionar uma solicitação de trabalho de leitura assíncrona ao io_service (3)
adicionar manipulador de impressão a io_service (4)
execute o io_service (5)
há trabalho ou manipuladores?
sim, há 1 trabalho e 2 manipuladores
o socket tem dados? não faça nada
execute o gerenciador de impressão (1)
há trabalho ou manipuladores?
sim, há 1 trabalho e 1 manipulador
o socket tem dados? não faça nada
execute o manipulador de impressão (4)
há trabalho ou manipuladores?
sim, há 1 trabalho
o socket tem dados? não, continue esperando
- socket recebe dados -
socket tem dados, leia no buffer
adicionar manipulador handle_async_receive a io_service
há trabalho ou manipuladores?
sim, há 1 manipulador
execute o manipulador handle_async_receive (3)
há trabalho ou manipuladores?
não, defina io_service como interrompido e retornar
Observe como, quando a leitura terminou, ela adicionou outro manipulador ao io_service
. Esse detalhe sutil é um recurso importante da programação assíncrona. Ele permite que os manipuladores sejam encadeados. Por exemplo, se handle_async_receive
não obtiver todos os dados esperados, sua implementação poderá postar outra operação de leitura assíncrona, resultando em io_service
mais trabalho e, portanto, não retornando io_service.run()
.
Note que quando o io_service
tem correu para fora do trabalho, o aplicativo deve reset()
a io_service
antes de executá-lo novamente.
Pergunta de exemplo e código do exemplo 3a
Agora, vamos examinar as duas partes do código mencionadas na pergunta.
Código da Pergunta
socket->async_receive
adiciona trabalho ao io_service
. Portanto, io_service->run()
bloqueará até que a operação de leitura seja concluída com sucesso ou erro e ClientReceiveEvent
tenha concluído a execução ou gere uma exceção.
Na esperança de tornar mais fácil de entender, aqui está um exemplo menor anotado 3a:
void CalculateFib(std::size_t n);
int main()
{
boost::asio::io_service io_service;
boost::optional<boost::asio::io_service::work> work =
boost::in_place(boost::ref(io_service));
boost::thread_group worker_threads;
for(int x = 0; x < 2; ++x)
{
worker_threads.create_thread(
boost::bind(&boost::asio::io_service::run, &io_service)
);
}
io_service.post(boost::bind(CalculateFib, 3));
io_service.post(boost::bind(CalculateFib, 4));
io_service.post(boost::bind(CalculateFib, 5));
work = boost::none;
worker_threads.join_all();
}
Em um alto nível, o programa criará 2 threads que processarão o io_service
loop de eventos (2). Isso resulta em um pool de threads simples que calculará os números de Fibonacci (3).
A única diferença principal entre o código de pergunta e esse código é que ele invoca io_service::run()
(2) antes que o trabalho real e os manipuladores sejam adicionados a io_service
(3). Para evitar que o io_service::run()
retorne imediatamente, um io_service::work
objeto é criado (1). Este objeto evita que o io_service
trabalho fique sem trabalho; portanto, io_service::run()
não retornará como resultado de nenhum trabalho.
O fluxo geral é o seguinte:
- Crie e adicione o
io_service::work
objeto adicionado ao io_service
.
- Conjunto de threads criado que invoca
io_service::run()
. Esses threads de trabalho não retornarão por io_service
causa do io_service::work
objeto.
- Adicione 3 manipuladores que calculam os números de Fibonacci ao
io_service
e retorne imediatamente. Os threads de trabalho, não o thread principal, podem começar a executar esses manipuladores imediatamente.
- Exclua o
io_service::work
objeto.
- Aguarde o término da execução dos threads de trabalho. Isso só ocorrerá quando todos os 3 manipuladores concluírem a execução, pois
io_service
nenhum deles tem manipuladores nem trabalho.
O código poderia ser escrito de forma diferente, da mesma maneira que o Código Original, onde manipuladores são adicionados ao io_service
e, em seguida, o io_service
loop de evento é processado. Isso elimina a necessidade de uso io_service::work
e resulta no seguinte código:
int main()
{
boost::asio::io_service io_service;
io_service.post(boost::bind(CalculateFib, 3));
io_service.post(boost::bind(CalculateFib, 4));
io_service.post(boost::bind(CalculateFib, 5));
boost::thread_group worker_threads;
for(int x = 0; x < 2; ++x)
{
worker_threads.create_thread(
boost::bind(&boost::asio::io_service::run, &io_service)
);
}
worker_threads.join_all();
}
Síncrono vs. Assíncrono
Embora o código em questão esteja usando uma operação assíncrona, ele está efetivamente funcionando de forma síncrona, pois está aguardando a conclusão da operação assíncrona:
socket.async_receive(buffer, handler)
io_service.run();
é equivalente a:
boost::asio::error_code error;
std::size_t bytes_transferred = socket.receive(buffer, 0, error);
handler(error, bytes_transferred);
Como regra geral, tente evitar misturar operações síncronas e assíncronas. Muitas vezes, pode transformar um sistema complexo em um sistema complicado. Essa resposta destaca as vantagens da programação assíncrona, algumas das quais também são abordadas na documentação do Boost.Asio .