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
print
conduttore (1).
- Il
handle_async_receive
conduttore (3).
- Il
print
conduttore (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_service
e 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_service
che funziona deve essere fatto. L' async_receive
operazione (3) informa io_service
che dovrà leggere in modo asincrono i dati dal socket, quindi async_receive
ritorna immediatamente.
- Fare il lavoro vero e proprio. In questo caso, quando
socket
riceve 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
print
gestori, l'operazione di ricezione viene completata con esito positivo o negativo e il relativo handle_async_receive
gestore è 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_receive
non ha ottenuto tutti i dati previsti, la sua implementazione potrebbe pubblicare un'altra operazione di lettura asincrona, risultando in io_service
più lavoro e quindi senza ritorno da io_service.run()
.
Fare notare che quando l' io_service
ha esaurito il lavoro, l'applicazione deve reset()
la io_service
prima 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_receive
aggiunge 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 ClientReceiveEvent
avrà 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_service
ciclo 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::work
viene creato un oggetto (1). Questo oggetto impedisce che il io_service
lavoro si esaurisca; pertanto, io_service::run()
non tornerà come risultato di nessun lavoro.
Il flusso complessivo è il seguente:
- Crea e aggiungi l'
io_service::work
oggetto aggiunto al file io_service
.
- Pool di thread creato che invoca
io_service::run()
. Questi thread di lavoro non verranno restituiti a io_service
causa io_service::work
dell'oggetto.
- Aggiungi 3 gestori che calcolano i numeri di Fibonacci a
io_service
e torna immediatamente. I thread di lavoro, non il thread principale, possono iniziare a eseguire questi gestori immediatamente.
- Elimina l'
io_service::work
oggetto.
- 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_service
nessuno 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_service
viene elaborato il ciclo di eventi. Ciò elimina la necessità di utilizzare io_service::work
e 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 .