Confuso quando boost :: asio :: io_service esegue il metodo blocca / sblocca

Essendo un principiante assoluto su Boost.Asio, sono confuso con io_service::run() . Lo apprezzerei se qualcuno potesse spiegarmi quando questo metodo blocca / sblocca. La documentazione afferma:

I blocchi di funzione run() fino a quando tutto il lavoro è terminato e non ci sono più gestori da inviare, o fino a quando il servizio io_service è stato arrestato.

Più thread possono chiamare la funzione run() per impostare un pool di thread da cui il servizio io_service può eseguire i gestori. Tutti i thread che stanno aspettando nel pool sono equivalenti e il io_service può scegliere uno di essi per richiamare un gestore.

Una normale uscita dalla funzione run() implica che l’object io_service sia arrestato (la funzione stopped() restituisce true). Le chiamate successive a run() , run_one() , poll() o poll_one() torneranno immediatamente a meno che non ci sia una chiamata precedente a reset() .

Cosa significa la seguente affermazione?

[…] non più gestori da inviare […]


Durante il tentativo di comprendere il comportamento di io_service::run() , ho trovato questo esempio (esempio 3a). Al suo interno, osservo che io_service->run() blocca e attende gli ordini di lavoro.

 // WorkerThread invines io_service->run() void WorkerThread(boost::shared_ptr io_service); void CalculateFib(size_t); boost::shared_ptr io_service( new boost::asio::io_service); boost::shared_ptr work( new boost::asio::io_service::work(*io_service)); // ... boost::thread_group worker_threads; for(int x = 0; x post( boost::bind(CalculateFib, 3)); io_service->post( boost::bind(CalculateFib, 4)); io_service->post( boost::bind(CalculateFib, 5)); work.reset(); worker_threads.join_all(); 

Tuttavia, nel seguente codice su cui stavo lavorando, il client si connette tramite TCP / IP e il metodo di esecuzione blocca fino a quando i dati non vengono ricevuti in modo asincrono.

 typedef boost::asio::ip::tcp tcp; boost::shared_ptr io_service( new boost::asio::io_service); boost::shared_ptr socket(new tcp::socket(*io_service)); // Connect to 127.0.0.1:9100. tcp::resolver resolver(*io_service); tcp::resolver::query query("127.0.0.1", boost::lexical_cast(9100)); tcp::resolver::iterator endpoint_iterator = resolver.resolve(query); socket->connect(endpoint_iterator->endpoint()); // Just blocks here until a message is received. socket->async_receive(boost::asio::buffer(buf_client, 3000), 0, ClientReceiveEvent); io_service->run(); // Write response. boost::system::error_code ignored_error; std::cout << "Sending message \n"; boost::asio::write(*socket, boost::asio::buffer("some data"), ignored_error); 

Qualsiasi spiegazione di run() che descrive il suo comportamento nei due esempi seguenti sarà apprezzata.

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); // 1 socket.connect(endpoint); // 2 socket.async_receive(buffer, &handle_async_receive); // 3 io_service.post(&print); // 4 io_service.run(); // 5 

Cos’è un gestore ?

Un gestore non è altro che una richiamata. Nel codice di esempio, ci sono 3 gestori:

  • Il gestore di print (1).
  • Il gestore handle_async_receive (3).
  • Il gestore di print (4).

Anche se la stessa funzione print() viene utilizzata due volte, ogni utilizzo viene considerato per creare il proprio gestore univoco identificabile. Gli handler possono essere di varie forms e dimensioni, dalle funzioni di base come quelle sopra ai costrutti più complessi come i funtori generati da boost::bind() e lambda. Indipendentemente dalla complessità, il gestore rimane ancora nient’altro che un callback.

Cosa è il lavoro ?

Il lavoro è un processo che è stato richiesto a Boost.Asio per conto del codice dell’applicazione. A volte Boost.Asio può avviare parte del lavoro non appena ne è stato informato, e altre volte potrebbe attendere di eseguire il lavoro in un secondo momento. Una volta terminato il lavoro, Boost.Asio informsrà l’applicazione invocando il gestore fornito.

Boost.Asio garantisce che i gestori eseguano solo all’interno di un thread che sta attualmente chiamando run() , run_one() , poll() o poll_one() . Questi sono i thread che funzionano e chiamano i gestori . Pertanto, nell’esempio sopra, print() non viene invocato quando viene pubblicato in io_service (1). Invece, viene aggiunto a io_service e verrà richiamato in un secondo momento. In questo caso, all’interno di io_service.run() (5).

Cosa sono le operazioni asincrone?

Un’operazione asincrona crea lavoro e Boost.Asio invocherà un gestore per informare l’applicazione al termine del lavoro. Le operazioni asincrone vengono create chiamando una funzione che ha un nome con il prefisso async_ . Queste funzioni sono anche note come funzioni di avvio .

Le operazioni asincrone possono essere scomposte in tre passaggi univoci:

  • Iniziando o informando, il servizio io_service associato che funziona deve essere fatto. L’operazione async_receive (3) informa il io_service che dovrà leggere i dati in modo asincrono dal socket, quindi async_receive restituirà immediatamente.
  • Fare il lavoro reale. In questo caso, quando il socket riceve i dati, i byte verranno letti e copiati nel buffer . Il lavoro effettivo sarà fatto in entrambi:
    • La funzione di avvio (3), se Boost.Asio può determinare che non bloccherà.
    • Quando l’applicazione esegue esplicitamente il servizio io_service (5).
  • Richiamo di handle_async_receive ReadHandler . Ancora una volta, i gestori vengono richiamati solo all’interno dei thread che eseguono io_service . Pertanto, indipendentemente dal momento in cui il lavoro è stato eseguito (3 o 5), è garantito che handle_async_receive() verrà invocato solo all’interno di io_service.run() (5).

La separazione nel tempo e nello spazio tra questi tre passaggi è nota come inversione del stream di controllo. È una delle complessità che rende difficile la programmazione asincrona. Tuttavia, ci sono tecniche che possono aiutare a mitigare questo, ad esempio usando le coroutine .

Cosa fa io_service.run() ?

Quando un thread chiama io_service.run() , il lavoro e i gestori verranno richiamati da questo thread. Nell’esempio sopra, io_service.run() (5) bloccherà fino a quando:

  • Ha richiamato e restituito da entrambi print gestori di print , l’operazione di ricezione si è conclusa con esito positivo o negativo e il suo gestore handle_async_receive è stato richiamato e restituito.
  • Il servizio io_service viene arrestato esplicitamente tramite io_service::stop() .
  • Un’eccezione viene generata da un gestore.

Un potenziale stream psuedo-ish potrebbe essere descritto come il seguente:

 crea io_service
 creare socket
 aggiungi il gestore di stampa a io_service (1)
 attendere che il socket si connetta (2)
 aggiungere una richiesta di lavoro di lettura asincrona a io_service (3)
 aggiungi il gestore di stampa a io_service (4)
 eseguire il io_service (5)
   c'è lavoro o handler?
     sì, c'è 1 lavoro e 2 gestori
       la presa ha dati?  no, non fare niente
       run print handler (1)
   c'è lavoro o handler?
     sì, c'è 1 lavoro e 1 gestore
       la presa ha dati?  no, non fare niente
       eseguire print handler (4)
   c'è lavoro o handler?
     sì, c'è 1 lavoro
       la presa ha dati?  no, continua ad aspettare
   - Il socket riceve i dati -
       socket ha dati, leggerli nel buffer
       aggiungere il gestore handle_async_receive a io_service
   c'è lavoro o handler?
     sì, c'è 1 gestore
       eseguire handle_async_receive handler (3)
   c'è lavoro o handler?
     no, imposta io_service come arrestato e restituisce 

Si noti come, al termine della lettura, è stato aggiunto un altro gestore al servizio io_service . Questo sottile dettaglio è una caratteristica importante della programmazione asincrona. Consente agli handler di essere incatenati insieme. Ad esempio, se handle_async_receive non ha ottenuto tutti i dati che si aspettava, la sua implementazione potrebbe postare un’altra operazione di lettura asincrona, con il risultato che io_service ha più lavoro e quindi non ritorna da io_service.run() .

Si noti che quando il io_service ha esaurito il lavoro, l’applicazione deve reset() il servizio io_service prima di eseguirlo di nuovo.


Esempio di domanda e codice di esempio 3a

Ora, esaminiamo i due pezzi di codice a cui si fa riferimento nella domanda.

Codice della domanda

socket->async_receive aggiunge lavoro a io_service . Pertanto, io_service->run() bloccherà fino a quando l’operazione di lettura non verrà completata con successo o errore, e ClientReceiveEvent ha terminato l’esecuzione o genera un’eccezione.

Esempio 3a Codice

Nella speranza di renderlo più facile da capire, ecco un Esempio 3a annotato più piccolo:

 void CalculateFib(std::size_t n); int main() { boost::asio::io_service io_service; boost::optional 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 } 

Ad un livello superiore, il programma creerà 2 thread che elaboreranno il io_service degli eventi di io_service (2). Ciò si traduce in un pool di thread semplice 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 i gestori vengano aggiunti a io_service (3). Per evitare il ritorno immediato di io_service::run() , viene creato un object di io_service::work (1). Questo object impedisce a io_service di io_service senza lavoro; quindi, io_service::run() non tornerà come risultato di nessun lavoro.

Il stream generale è il seguente:

  1. Creare e aggiungere l'object di io_service::work aggiunto a io_service .
  2. Pool di thread creato che richiama io_service::run() . Questi thread di lavoro non verranno restituiti da io_service causa dell'object io_service::work .
  3. Aggiungi 3 gestori che calcolano i numeri di Fibonacci sul servizio io_service e li restituiscono immediatamente. I thread worker, non il thread principale, possono avviare immediatamente l'esecuzione di questi gestori.
  4. Elimina l'object io_service::work .
  5. Attendere il termine dell'esecuzione dei thread di lavoro. Questo avverrà solo quando tutti e 3 i gestori avranno terminato l'esecuzione, poiché il io_service non ha né gestori né lavoro.

Il codice potrebbe essere scritto in modo diverso, allo stesso modo del codice originale, dove i gestori sono aggiunti al servizio io_service , e quindi il ciclo di eventi io_service viene elaborato. Ciò elimina la necessità di utilizzare io_service::work e produce il seguente codice:

 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 } 

Sincrono contro asincrono

Sebbene il codice nella domanda stia utilizzando 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 di Boost.Asio.

Per semplificare come run , pensalo come un impiegato che deve elaborare una pila di carta; prende un foglio, fa quello che dice il foglio, getta via il foglio e prende quello successivo; quando finisce le lenzuola, lascia l’ufficio. Su ogni foglio ci può essere qualsiasi tipo di istruzione, anche aggiungendo un nuovo foglio alla pila. Torna ad asio: puoi dare un lavoro a io_service in due modi, essenzialmente: usando il post su di esso come nell’esempio che hai collegato, o usando altri oggetti che chiamano internamente post sul io_service , come il socket e i suoi metodi async_* .