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


88

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

La run()funzione si blocca finché tutto il lavoro non è terminato e non ci sono più gestori da inviare, o finché non io_serviceè stato arrestato.

Più thread possono chiamare la run()funzione per impostare un pool di thread da cui io_servicepossono eseguire gestori. Tutti i thread in attesa nel pool sono equivalenti e io_servicepossono scegliere uno qualsiasi di essi per invocare un gestore.

Una normale uscita dalla run()funzione implica che l' io_serviceoggetto è fermo (la stopped()funzione restituisce true). Le chiamate successive a run(), run_one(), poll()o poll_one()torneranno immediatamente a meno che ci sia una chiamata prima reset().

Cosa significa la seguente dichiarazione?

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


Mentre cercavo di capire il comportamento di io_service::run(), mi sono imbattuto in questo esempio (esempio 3a). Al suo interno, osservo che si io_service->run()blocca e attende gli ordini di lavoro.

// WorkerThread invines io_service->run()
void WorkerThread(boost::shared_ptr<boost::asio::io_service> io_service);
void CalculateFib(size_t);

boost::shared_ptr<boost::asio::io_service> io_service(
    new boost::asio::io_service);
boost::shared_ptr<boost::asio::io_service::work> work(
   new boost::asio::io_service::work(*io_service));

// ...

boost::thread_group worker_threads;
for(int x = 0; x < 2; ++x)
{
  worker_threads.create_thread(boost::bind(&WorkerThread, io_service));
}

io_service->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 codice seguente su cui stavo lavorando, il client si connette utilizzando TCP / IP e il metodo di esecuzione si blocca finché i dati non vengono ricevuti in modo asincrono.

typedef boost::asio::ip::tcp tcp;
boost::shared_ptr<boost::asio::io_service> io_service(
    new boost::asio::io_service);
boost::shared_ptr<tcp::socket> 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< std::string >(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);

Qualunque spiegazione run()che ne descriva il comportamento nei due esempi seguenti sarebbe apprezzata.

Risposte:


234

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 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.

Codice di esempio 3a

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 =       // '. 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 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:

  1. Crea e aggiungi l' io_service::workoggetto aggiunto al file io_service.
  2. Pool di thread creato che invoca io_service::run(). Questi thread di lavoro non verranno restituiti a io_servicecausa io_service::workdell'oggetto.
  3. 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.
  4. Elimina l' io_service::workoggetto.
  5. 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));              //   :- 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 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 .


13
Post fantastico. Vorrei aggiungere solo una cosa perché penso che non riceva abbastanza attenzione: dopo che run () è tornato, devi chiamare reset () sul tuo io_service prima di poterlo eseguire di nuovo. Altrimenti potrebbe tornare istantaneamente indipendentemente dal fatto che vi siano o meno operazioni async_ in attesa o meno.
DeVadder

Da dove viene il buffer? Che cos'è?
ruipacheco

Sono ancora confuso. Se il missaggio è sincronizzato e asincrono non è consigliato, qual è la modalità asincrona pura? puoi fare un esempio che mostra il codice senza io_service.run () ;?
Splash

@Splash One può essere utilizzato io_service.poll()per elaborare il ciclo di eventi senza bloccare le operazioni in sospeso. Il consiglio principale per evitare di mischiare operazioni sincrone e asincrone è evitare di aggiungere complessità inutili e prevenire una scarsa reattività quando i gestori impiegano molto tempo per completare. Ci sono alcuni casi in cui è sicuro, ad esempio quando si sa che l'operazione sincrona non si bloccherà.
Tanner Sansbury

Cosa intendi con "attualmente" in "Boost.Asio garantisce che i gestori funzioneranno solo all'interno di un thread che sta attualmente chiamandorun() ...." ? Se ci sono N thread (che ha chiamato run()), qual è il thread "corrente"? Ce ne possono essere tanti? O vuoi dire che il thread che ha terminato di eseguire async_*()(diciamo async_read), è garantito che chiami anche i suoi gestori?
Nawaz

18

Per semplificare il modo in cui cosa runfa, pensalo come un dipendente che deve elaborare una pila di carta; prende un foglio, fa quello che dice il foglio, butta via il foglio e prende quello successivo; quando esaurisce le lenzuola, lascia l'ufficio. Su ogni foglio può esserci qualsiasi tipo di istruzione, anche l'aggiunta di un nuovo foglio alla pila. Tornando ad asio: puoi dare a io_serviceun'opera in due modi, essenzialmente: usandola postcome nell'esempio che hai linkato, oppure usando altri oggetti che richiamano internamente postil io_service, come il sockete i suoi async_*metodi.

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.