Fondazione
Iniziamo con un esempio semplificato ed esaminiamo i pezzi Boost.Asio rilevanti:
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();
Cos'è un gestore ?
Un gestore non è altro che un callback. Nel codice di esempio, ci sono 3 gestori:
- Il
printconduttore (1).
- Il
handle_async_receiveconduttore (3).
- Il
printconduttore (4).
Anche se la stessa print()funzione viene utilizzata due volte, si considera che ogni utilizzo crei il proprio gestore identificabile in modo univoco. I gestori possono avere molte forme e dimensioni, che vanno da funzioni di base come quelle sopra a costrutti più complessi come funtori generati da boost::bind()e lambda. Indipendentemente dalla complessità, il gestore rimane ancora nient'altro che un callback.
Cos'è il lavoro ?
Il lavoro è un'elaborazione che Boost.Asio è stato richiesto di eseguire per conto del codice dell'applicazione. A volte Boost.Asio può iniziare una parte del lavoro non appena gli è stato detto, e altre volte può aspettare per completare il lavoro in un secondo momento. Una volta terminato il lavoro, Boost.Asio informerà l'applicazione invocando il gestore fornito .
Boost.Asio garantisce che i gestori verranno eseguiti solo all'interno di un filo che è attualmente chiamando run(), run_one(), poll(), o poll_one(). Questi sono i thread che funzioneranno e chiameranno i gestori . Pertanto, nell'esempio precedente, print()non viene richiamato quando viene pubblicato in io_service(1). Viene invece aggiunto al io_servicee verrà richiamato in un secondo momento. In questo caso, entro io_service.run()(5).
Cosa sono le operazioni asincrone?
Un'operazione asincrona crea lavoro e Boost.Asio chiamerà un gestore per informare l'applicazione quando il lavoro è stato completato. Le operazioni asincrone vengono create chiamando una funzione che ha un nome con il prefisso async_. Queste funzioni sono note anche come funzioni di avvio .
Le operazioni asincrone possono essere scomposte in tre passaggi unici:
- Avviare, o informare, l'associato
io_serviceche funziona deve essere fatto. L' async_receiveoperazione (3) informa io_serviceche dovrà leggere in modo asincrono i dati dal socket, quindi async_receiveritorna immediatamente.
- Fare il lavoro vero e proprio. In questo caso, quando
socketriceve i dati, i byte verranno letti e copiati in buffer. Il lavoro effettivo verrà svolto in:
- La funzione iniziale (3), se Boost.Asio può determinare che non si bloccherà.
- Quando l'applicazione esegue esplicitamente il
io_service(5).
- Invocando la
handle_async_receive ReadHandler . Ancora una volta, i gestori vengono richiamati solo all'interno di thread che eseguono l'estensione io_service. Pertanto, indipendentemente da quando il lavoro è stato svolto (3 o 5), è garantito che handle_async_receive()verrà richiamato solo entro io_service.run()(5).
La separazione nel tempo e nello spazio tra questi tre passaggi è nota come inversione del flusso di controllo. È una delle complessità che rende difficile la programmazione asincrona. Tuttavia, esistono tecniche che possono aiutare a mitigare questo problema, ad esempio utilizzando le coroutine .
Cosa fa io_service.run()?
Quando un thread chiama io_service.run(), lavoro e gestori verranno richiamati dall'interno di questo thread. Nell'esempio precedente, io_service.run()(5) si bloccherà fino a quando:
- È stato richiamato e restituito da entrambi i
printgestori, l'operazione di ricezione viene completata con esito positivo o negativo e il relativo handle_async_receivegestore è stato richiamato e restituito.
- Il
io_serviceè esplicitamente arrestato mediante io_service::stop().
- Viene generata un'eccezione dall'interno di un gestore.
Un potenziale flusso psuedo-ish potrebbe essere descritto come il seguente:
creare io_service
creare socket
aggiungi il gestore di stampa a io_service (1)
attendere il collegamento del socket (2)
aggiungi una richiesta di lavoro di lettura asincrona a io_service (3)
aggiungi il gestore di stampa a io_service (4)
esegui io_service (5)
c'è lavoro o gestori?
si, c'è 1 opera e 2 gestori
socket ha dati? no, non fare niente
eseguire il gestore di stampa (1)
c'è lavoro o gestori?
si, c'è 1 operaio e 1 conduttore
socket ha dati? no, non fare niente
eseguire il gestore di stampa (4)
c'è lavoro o gestori?
sì, c'è 1 opera
socket ha dati? no, continua ad aspettare
- il socket riceve i dati -
socket ha dati, leggili nel buffer
aggiungi il gestore handle_async_receive a io_service
c'è lavoro o gestori?
sì, c'è 1 gestore
eseguire il gestore handle_async_receive (3)
c'è lavoro o gestori?
no, imposta io_service come fermato e ritorna
Si noti come quando la lettura è terminata, ha aggiunto un altro gestore al file io_service. Questo sottile dettaglio è una caratteristica importante della programmazione asincrona. Consente agli handler di essere concatenati insieme. Ad esempio, se handle_async_receivenon ha ottenuto tutti i dati previsti, la sua implementazione potrebbe pubblicare un'altra operazione di lettura asincrona, risultando in io_servicepiù lavoro e quindi senza ritorno da io_service.run().
Fare notare che quando l' io_serviceha esaurito il lavoro, l'applicazione deve reset()la io_serviceprima di eseguirlo di nuovo.
Esempio di domanda ed esempio 3a codice
Ora, esaminiamo i due pezzi di codice a cui si fa riferimento nella domanda.
Codice domanda
socket->async_receiveaggiunge lavoro a io_service. Pertanto, io_service->run()si bloccherà fino a quando l'operazione di lettura non verrà completata con successo o errore e non ClientReceiveEventavrà terminato l'esecuzione o non verrà generata un'eccezione.
Nella speranza di renderlo più facile da capire, ecco un piccolo esempio annotato 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();
}
Ad alto livello, il programma creerà 2 thread che elaboreranno il io_serviceciclo di eventi di (2). Ciò si traduce in un semplice pool di thread che calcolerà i numeri di Fibonacci (3).
L'unica differenza principale tra il codice domanda e questo codice è che questo codice richiama io_service::run()(2) prima che il lavoro effettivo e gli handler vengano aggiunti al io_service(3). Per evitare che io_service::run()ritorni immediatamente, io_service::workviene creato un oggetto (1). Questo oggetto impedisce che il io_servicelavoro si esaurisca; pertanto, io_service::run()non tornerà come risultato di nessun lavoro.
Il flusso complessivo è il seguente:
- Crea e aggiungi l'
io_service::workoggetto aggiunto al file io_service.
- Pool di thread creato che invoca
io_service::run(). Questi thread di lavoro non verranno restituiti a io_servicecausa io_service::workdell'oggetto.
- Aggiungi 3 gestori che calcolano i numeri di Fibonacci a
io_servicee torna immediatamente. I thread di lavoro, non il thread principale, possono iniziare a eseguire questi gestori immediatamente.
- Elimina l'
io_service::workoggetto.
- Attendi il completamento dell'esecuzione dei thread di lavoro. Ciò si verificherà solo quando tutti e 3 i gestori avranno terminato l'esecuzione, poiché
io_servicenessuno dei due ha gestori né lavora.
Il codice potrebbe essere scritto in modo diverso, allo stesso modo del codice originale, in cui vengono aggiunti i gestori a io_service, quindi io_serviceviene elaborato il ciclo di eventi. Ciò elimina la necessità di utilizzare io_service::worke risulta nel codice seguente:
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();
}
Sincrono vs. asincrono
Sebbene il codice nella domanda utilizzi un'operazione asincrona, funziona effettivamente in modo sincrono, poiché è in attesa del completamento dell'operazione asincrona:
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);
Come regola generale, cerca di evitare di mischiare operazioni sincrone e asincrone. Spesso, può trasformare un sistema complesso in un sistema complicato. Questa risposta evidenzia i vantaggi della programmazione asincrona, alcuni dei quali sono anche trattati nella documentazione Boost.Asio .