L'I / O non bloccante è davvero più veloce dell'I / O con blocco multi-thread? Come?


119

Ho cercato sul Web alcuni dettagli tecnici sul blocco dell'I / O e sull'I / O non bloccante e ho trovato diverse persone che affermavano che l'I / O non bloccante sarebbe stato più veloce del blocco dell'I / O. Ad esempio in questo documento .

Se uso l'I / O di blocco, ovviamente il thread attualmente bloccato non può fare nient'altro ... Perché è bloccato. Ma non appena un thread inizia a essere bloccato, il sistema operativo può passare a un altro thread e non tornare indietro finché non c'è qualcosa da fare per il thread bloccato. Quindi fintanto che c'è un altro thread sul sistema che necessita di CPU e non è bloccato, non dovrebbe esserci più tempo di inattività della CPU rispetto a un approccio non bloccante basato su eventi, vero?

Oltre a ridurre il tempo di inattività della CPU, vedo un'opzione in più per aumentare il numero di attività che un computer può eseguire in un dato intervallo di tempo: ridurre l'overhead introdotto dal cambio di thread. Ma come si può fare? Ed è l'overhead abbastanza grande da mostrare effetti misurabili? Ecco un'idea su come immagino che funzioni:

  1. Per caricare il contenuto di un file, un'applicazione delega questa attività a un framework i / o basato su eventi, passando una funzione di callback insieme a un nome file
  2. Il framework degli eventi delega al sistema operativo, che programma un controller DMA del disco rigido per scrivere il file direttamente nella memoria
  3. Il framework degli eventi consente l'esecuzione di ulteriore codice.
  4. Al termine della copia da disco a memoria, il controller DMA provoca un interrupt.
  5. Il gestore di interrupt del sistema operativo notifica al framework i / o basato su eventi che il file è stato completamente caricato in memoria. Come lo fa? Usando un segnale ??
  6. Il codice attualmente eseguito all'interno del framework di i / o evento termina.
  7. Il framework i / o basato su eventi controlla la propria coda e vede il messaggio del sistema operativo dal passaggio 5 ed esegue il callback ottenuto nel passaggio 1.

È così che funziona? In caso contrario, come funziona? Ciò significa che il sistema degli eventi può funzionare senza mai la necessità di toccare esplicitamente lo stack (come un vero scheduler che avrebbe bisogno di eseguire il backup dello stack e copiare lo stack di un altro thread in memoria mentre si cambia thread)? Quanto tempo fa risparmiare effettivamente? C'è di più?


5
risposta breve: si tratta più del sovraccarico di avere un thread per connessione. io non bloccante consente di evitare di avere un thread per connessione.
Dan D.

10
Il blocco dell'I / O è costoso su un sistema in cui non è possibile creare tanti thread quante sono le connessioni. Sulla JVM puoi creare qualche migliaio di thread, ma cosa succede se hai più di 100.000 connessioni? Quindi devi attenersi a una soluzione asincrona. Tuttavia, ci sono linguaggi in cui i thread non sono costosi (ad esempio i thread verdi) come in Go / Erlang / Rust dove non è un problema avere 100.000 thread. Quando il numero di thread può essere elevato, credo che il blocco di IO produca tempi di risposta più rapidi. Ma questo è qualcosa che dovrei anche chiedere agli esperti se ciò è vero nella realtà.
OlliP

@OliverPlow, lo penso anch'io, perché bloccare l'IO di solito significa lasciare che il sistema gestisca la "gestione parallela", invece di farlo da soli usando code di attività e simili.
Pacerier

1
@ DanD., E se l'overhead di avere thread è uguale all'overhead di eseguire IO non bloccanti? (di solito vero nel caso dei fili verdi)
Pacerier

"copiare lo stack" non avviene. Thread diversi hanno le loro pile a indirizzi diversi. Ogni thread ha il proprio puntatore allo stack, insieme ad altri registri. Un cambio di contesto salva / ripristina solo lo stato dell'architettura (inclusi tutti i registri), ma non la memoria. Tra i thread nello stesso processo, il kernel non deve nemmeno cambiare le tabelle delle pagine.
Peter Cordes,

Risposte:


44

Il più grande vantaggio dell'I / O non bloccante o asincrono è che il thread può continuare il suo lavoro in parallelo. Ovviamente puoi ottenere questo risultato anche utilizzando un thread aggiuntivo. Come hai affermato per le migliori prestazioni complessive (di sistema), immagino che sarebbe meglio usare I / O asincrono e non più thread (riducendo così la commutazione dei thread).

Diamo un'occhiata alle possibili implementazioni di un programma per server di rete che gestirà 1000 client collegati in parallelo:

  1. Un thread per connessione (può bloccare I / O, ma può anche essere I / O non bloccante).
    Ogni thread richiede risorse di memoria (anche memoria del kernel!), Questo è uno svantaggio. E ogni thread aggiuntivo significa più lavoro per lo scheduler.
  2. Un thread per tutte le connessioni.
    Questo richiede carico dal sistema perché abbiamo meno thread. Ma ti impedisce anche di utilizzare tutte le prestazioni della tua macchina, perché potresti finire per portare un processore al 100% e lasciare tutti gli altri processori inattivi.
  3. Alcuni thread in cui ogni thread gestisce alcune delle connessioni.
    Questo richiede carico dal sistema perché ci sono meno thread. E può utilizzare tutti i processori disponibili. Su Windows questo approccio è supportato dall'API del pool di thread .

Ovviamente avere più thread non è di per sé un problema. Come avrai capito, ho scelto un numero piuttosto elevato di connessioni / thread. Dubito che vedrai differenze tra le tre possibili implementazioni se parliamo solo di una dozzina di thread (questo è anche ciò che Raymond Chen suggerisce nel post del blog MSDN Windows ha un limite di 2000 thread per processo? ).

Su Windows l'utilizzo di I / O di file senza buffer significa che le scritture devono avere una dimensione che è un multiplo della dimensione della pagina. Non l'ho testato, ma sembra che questo potrebbe anche influenzare positivamente le prestazioni di scrittura per le scritture sincrone e asincrone bufferizzate.

I passaggi da 1 a 7 che descrivi danno una buona idea di come funziona. Su Windows il sistema operativo ti informerà del completamento di un I / O asincrono ( WriteFilecon OVERLAPPEDstruttura) utilizzando un evento o un callback. Le funzioni di richiamata verranno chiamate ad esempio solo quando il codice chiama WaitForMultipleObjectsExcon bAlertableimpostato su true.

Ancora qualche lettura sul web:


Dal punto di vista web le conoscenze comuni (Internet, commenti di esperti) suggeriscono che aumentando notevolmente il max. il numero di thread di richiesta è una cosa negativa nel bloccare l'IO (rendendo l'elaborazione delle richieste ancora più lenta) a causa dell'aumento della memoria e del tempo di cambio di contesto, ma, Async IO non fa la stessa cosa quando rimanda il lavoro a un altro thread? Sì, ora puoi soddisfare più richieste ma avere lo stesso numero di thread in background .. qual è il vero vantaggio di ciò?
JavierJ

1
@JavierJ Sembri credere che se n thread eseguono l'IO di file asincrono verranno creati altri n thread per eseguire un blocco di IO? Questo non è vero. Il sistema operativo dispone del supporto IO per file asincrono e non è necessario bloccarlo durante l'attesa del completamento dell'IO. Può accodare le richieste di I / O e se si verifica un interrupt hardware (ad esempio DMA), può contrassegnare la richiesta come completata e impostare un evento che segnala il thread del chiamante. Anche se fosse richiesto un thread aggiuntivo, il sistema operativo sarebbe in grado di utilizzare quel thread per più richieste di I / O da più thread.
Werner Henze

Grazie, ha senso coinvolgere il supporto IO del file asincrono del sistema operativo ma quando scrivo codice per un'implementazione effettiva di questo (dal punto di vista web) dico con Java Servlet 3.0 NIO vedo ancora un thread per la richiesta e un thread in background ( async) in loop per leggere un file, un database o qualsiasi altra cosa.
JavierJ

1
@piyushGoyal ho riscritto la mia risposta. Spero sia più chiaro ora.
Werner Henze

1
Su Windows l'utilizzo di I / O di file asincrono significa che le scritture devono avere una dimensione che è un multiplo della dimensione della pagina. - no, non è così. Stai pensando a I / O senza buffer. (Sono spesso usati insieme, ma non devono esserlo.)
Harry Johnston,

29

I / O include più tipi di operazioni come la lettura e la scrittura di dati da dischi rigidi, l'accesso alle risorse di rete, la chiamata a servizi Web o il recupero di dati da database. A seconda della piattaforma e del tipo di operazione, l'I / O asincrono di solito trarrà vantaggio da qualsiasi supporto hardware o di sistema di basso livello per eseguire l'operazione. Ciò significa che verrà eseguito con il minor impatto possibile sulla CPU.

A livello di applicazione, l'I / O asincrono impedisce ai thread di dover attendere il completamento delle operazioni di I / O. Non appena viene avviata un'operazione di I / O asincrona, rilascia il thread su cui è stata avviata e viene registrato un callback. Al termine dell'operazione, il callback viene messo in coda per l'esecuzione sul primo thread disponibile.

Se l'operazione di I / O viene eseguita in modo sincrono, mantiene il thread in esecuzione che non esegue alcuna operazione fino al completamento dell'operazione. Il runtime non sa quando l'operazione di I / O viene completata, quindi fornirà periodicamente un po 'di tempo di CPU al thread in attesa, tempo di CPU che potrebbe altrimenti essere utilizzato da altri thread che hanno effettive operazioni associate alla CPU da eseguire.

Quindi, come citato da @ user1629468, l'I / O asincrono non fornisce prestazioni migliori ma piuttosto una migliore scalabilità. Questo è ovvio quando si esegue in contesti che hanno un numero limitato di thread disponibili, come nel caso delle applicazioni web. L'applicazione Web di solito utilizza un pool di thread da cui assegnano i thread a ciascuna richiesta. Se le richieste vengono bloccate su operazioni di I / O di lunga durata, c'è il rischio di esaurire il pool Web e di bloccare o rallentare la risposta dell'applicazione Web.

Una cosa che ho notato è che l'I / O asincrono non è l'opzione migliore quando si tratta di operazioni di I / O molto veloci. In tal caso il vantaggio di non mantenere un thread occupato in attesa del completamento dell'operazione di I / O non è molto importante e il fatto che l'operazione venga avviata su un thread e sia completata su un altro aggiunge un sovraccarico all'esecuzione complessiva.

Puoi leggere una ricerca più dettagliata che ho fatto di recente sull'argomento I / O asincrono rispetto al multithreading qui .


Mi chiedo se varrebbe la pena fare una distinzione tra le operazioni di I / O che ci si aspetta vengano completate e le cose che potrebbero non essere [es. "Ottenere il prossimo carattere che arriva su una porta seriale", nei casi in cui il dispositivo remoto potrebbe o meno inviare qualsiasi cosa]. Se si prevede che un'operazione di I / O venga completata entro un tempo ragionevole, si potrebbe ritardare la pulizia delle risorse correlate fino al completamento dell'operazione. Tuttavia, se l'operazione non fosse mai completata, tale ritardo sarebbe irragionevole.
supercat

@supercat lo scenario che stai descrivendo viene utilizzato in applicazioni e librerie di livello inferiore. I server fanno affidamento su di esso, poiché attendono continuamente le connessioni in entrata. L'I / O asincrono come descritto sopra non può rientrare in questo scenario perché si basa sull'avvio di un'operazione specifica e sulla registrazione di un callback per il suo completamento. Nel caso che stai descrivendo, devi registrare una richiamata su un evento di sistema ed elaborare ogni notifica. Stai continuamente elaborando input piuttosto che eseguire operazioni. Come detto, questo di solito viene fatto a basso livello, quasi mai nelle tue app.
Florin Dumitrescu

Il modello è abbastanza comune con le applicazioni fornite con vari tipi di hardware. Le porte seriali non sono così comuni come una volta, ma i chip USB che emulano le porte seriali sono piuttosto popolari nella progettazione di hardware specializzato. I caratteri di tali cose vengono gestiti a livello di applicazione, poiché il sistema operativo non avrà modo di sapere che una sequenza di caratteri di input significa, ad esempio, che un cassetto contanti è stato aperto e una notifica dovrebbe essere inviata da qualche parte.
supercat

Non penso che la parte relativa al costo della CPU del blocco dell'IO sia accurata: quando si trova nello stato di blocco, un thread che ha attivato l'IO di blocco viene messo in attesa dal sistema operativo e non costa periodi di CPU fino a quando l'IO non è completamente completato, solo dopo di che il sistema operativo (notifica tramite interruzioni) riprende il thread bloccato. Ciò che hai descritto (attesa occupata da un lungo polling) non è il modo in cui il blocco IO viene implementato in quasi tutti i runtime / compilatori.
Lifu Huang

4

Il motivo principale per utilizzare AIO è per la scalabilità. Se visti nel contesto di pochi thread, i vantaggi non sono evidenti. Ma quando il sistema scala fino a migliaia di thread, AIO offrirà prestazioni molto migliori. L'avvertenza è che la libreria AIO non dovrebbe introdurre ulteriori colli di bottiglia.


4

Per presumere un miglioramento della velocità dovuto a qualsiasi forma di multi-elaborazione, è necessario presumere che più attività basate sulla CPU vengano eseguite contemporaneamente su più risorse di elaborazione (generalmente core del processore) oppure che non tutte le attività si basino sull'uso simultaneo di la stessa risorsa - cioè, alcune attività possono dipendere da un sottocomponente del sistema (archiviazione su disco, diciamo) mentre alcune attività dipendono da un altro (ricevere comunicazioni da un dispositivo periferico) e altre ancora possono richiedere l'utilizzo di core del processore.

Il primo scenario viene spesso definito programmazione "parallela". Il secondo scenario è spesso indicato come programmazione "concorrente" o "asincrona", sebbene "concorrente" sia talvolta utilizzato anche per riferirsi al caso in cui si consente semplicemente a un sistema operativo di intercalare l'esecuzione di più attività, indipendentemente dal fatto che tale esecuzione debba posto in serie o se è possibile utilizzare più risorse per ottenere l'esecuzione parallela. In quest'ultimo caso, "concorrente" si riferisce generalmente al modo in cui l'esecuzione è scritta nel programma, piuttosto che dal punto di vista dell'effettiva simultaneità dell'esecuzione del compito.

È molto facile parlare di tutto questo con supposizioni tacite. Ad esempio, alcuni si affrettano a fare affermazioni come "L'I / O asincrono sarà più veloce dell'I / O multithread". Questa affermazione è dubbia per diversi motivi. Innanzitutto, potrebbe essere il caso che un determinato framework I / O asincrono sia implementato precisamente con il multi-threading, nel qual caso sono uno nella stessa cosa e non ha senso dire che un concetto "è più veloce" dell'altro .

In secondo luogo, anche nel caso in cui sia presente un'implementazione a thread singolo di un framework asincrono (come un ciclo di eventi a thread singolo), è comunque necessario fare un'ipotesi su cosa sta facendo quel ciclo. Ad esempio, una cosa sciocca che puoi fare con un ciclo di eventi a thread singolo è richiedere che completi in modo asincrono due diverse attività puramente legate alla CPU. Se lo facessi su una macchina con solo un singolo core idealizzato del processore (ignorando le moderne ottimizzazioni hardware), allora l'esecuzione di questa attività "in modo asincrono" non funzionerebbe in modo diverso rispetto a eseguirla con due thread gestiti in modo indipendente o con un solo processo solitario - - la differenza potrebbe dipendere dal cambio di contesto del thread o dalle ottimizzazioni della pianificazione del sistema operativo, ma se entrambe le attività andranno alla CPU, sarebbe simile in entrambi i casi.

È utile immaginare molti casi insoliti o stupidi in cui potresti imbatterti.

"Asincrono" non deve essere simultaneo, ad esempio proprio come sopra: si eseguono "in modo asincrono" due attività legate alla CPU su una macchina con esattamente un core del processore.

L'esecuzione multi-threaded non deve essere simultanea: si generano due thread su una macchina con un singolo core del processore o si chiedono a due thread di acquisire qualsiasi altro tipo di risorsa scarsa (si immagini, ad esempio, un database di rete che può stabilirne solo uno connessione alla volta). L'esecuzione dei thread potrebbe essere intercalata, tuttavia lo scheduler del sistema operativo lo ritiene opportuno, ma il loro runtime totale non può essere ridotto (e sarà aumentato dal cambio di contesto del thread) su un singolo core (o più in generale, se si generano più thread di quanti ce ne siano core per eseguirli o avere più thread che richiedono una risorsa rispetto a ciò che la risorsa può sostenere). La stessa cosa vale anche per l'elaborazione multipla.

Quindi né l'I / O asincrono né il multi-threading devono offrire alcun miglioramento delle prestazioni in termini di tempo di esecuzione. Possono persino rallentare le cose.

Se si definisce un caso d'uso specifico, tuttavia, come un programma specifico che effettua una chiamata di rete per recuperare i dati da una risorsa connessa alla rete come un database remoto e fa anche alcuni calcoli locali legati alla CPU, è possibile iniziare a ragionare su le differenze di prestazioni tra i due metodi data una particolare ipotesi sull'hardware.

Le domande da porsi: quanti passaggi di calcolo devo eseguire e quanti sistemi di risorse indipendenti ci sono per eseguirli? Esistono sottoinsiemi delle fasi di calcolo che richiedono l'utilizzo di sottocomponenti di sistema indipendenti e possono trarre vantaggio dal farlo contemporaneamente? Quanti core del processore ho e qual è il sovraccarico per l'utilizzo di più processori o thread per completare le attività su core separati?

Se le tue attività si basano in gran parte su sottosistemi indipendenti, una soluzione asincrona potrebbe essere buona. Se il numero di thread necessari per gestirlo fosse elevato, in modo tale che il cambio di contesto diventasse non banale per il sistema operativo, una soluzione asincrona a thread singolo potrebbe essere migliore.

Ogni volta che le attività sono vincolate dalla stessa risorsa (ad es. Più necessità di accedere contemporaneamente alla stessa rete o risorsa locale), il multi-threading probabilmente introdurrà un sovraccarico insoddisfacente, e mentre l'asincronia a thread singolo potrebbe introdurre meno overhead, in tale risorsa- situazione limitata anch'essa non può produrre un'accelerazione. In tal caso, l'unica opzione (se si desidera aumentare la velocità) è rendere disponibili più copie di quella risorsa (ad esempio più core del processore se la risorsa scarsa è la CPU; un database migliore che supporta più connessioni simultanee se la risorsa scarsa è un database con limitazioni di connessione, ecc.).

Un altro modo per dirlo è: consentire al sistema operativo di intercalare l'utilizzo di una singola risorsa per due attività non può essere più veloce che lasciare semplicemente che un'attività utilizzi la risorsa mentre l'altra attende, quindi lasciare che la seconda attività finisca in serie. Inoltre, il costo dello scheduler dell'interleaving significa che in qualsiasi situazione reale crea effettivamente un rallentamento. Non importa se si verifica l'utilizzo interlacciato della CPU, di una risorsa di rete, di una risorsa di memoria, di un dispositivo periferico o di qualsiasi altra risorsa di sistema.


2

Una possibile implementazione dell'I / O non bloccante è esattamente quello che hai detto, con un pool di thread in background che bloccano l'I / O e notificano il thread del creatore dell'I / O tramite un meccanismo di callback. In effetti, è così che funziona il modulo AIO in glibc. Ecco alcuni vaghi dettagli sull'implementazione.

Sebbene questa sia una buona soluzione abbastanza portabile (purché si disponga di thread), il sistema operativo è in genere in grado di servire l'I / O non bloccante in modo più efficiente. Questo articolo di Wikipedia elenca le possibili implementazioni oltre al pool di thread.


2

Attualmente sto implementando async io su una piattaforma embedded utilizzando protothread. L'IO non bloccante fa la differenza tra l'esecuzione a 16000 fps e 160 fps. Il più grande vantaggio del non blocco io è che puoi strutturare il tuo codice per fare altre cose mentre l'hardware fa le sue cose. Anche l'inizializzazione dei dispositivi può essere eseguita in parallelo.

balestruccio


1

In Node, vengono avviati più thread, ma è uno strato inferiore nel runtime di C ++.

"Quindi sì, NodeJS è a thread singolo, ma questa è una mezza verità, in realtà è guidato da eventi e single-threaded con operatori in background. Il ciclo di eventi principale è a thread singolo ma la maggior parte dei lavori di I / O viene eseguita su thread separati, perché le API di I / O in Node.js sono asincrone / non bloccanti per progettazione, al fine di accogliere il ciclo di eventi. "

https://codeburst.io/how-node-js-single-thread-mechanism-work-understanding-event-loop-in-nodejs-230f7440b0ea

"Node.js non blocca, il che significa che tutte le funzioni (callback) sono delegate al ciclo di eventi e sono (o possono essere) eseguite da thread diversi. Questo è gestito dal run-time di Node.js."

https://itnext.io/multi-threading-and-multi-process-in-node-js-ffa5bb5cde98 

La spiegazione "Il nodo è più veloce perché non blocca ..." è un po 'di marketing e questa è un'ottima domanda. È efficiente e scalabile, ma non esattamente a thread singolo.


0

Il miglioramento per quanto ne so è che usi I / O asincrono (sto parlando di MS System, tanto per chiarire) il modo chiamato I / O porte di completamento . Usando la chiamata asincrona, il framework sfrutta automaticamente tale architettura e questo dovrebbe essere molto più efficiente del meccanismo di threading standard. Come esperienza personale posso dire che sentireste sensibilmente la vostra applicazione più reattiva se preferite AsyncCalls invece di bloccare i thread.


0

Consentitemi di darvi un controesempio che l'I / O asincrono non funziona. Sto scrivendo un proxy simile al seguente, utilizzando boost :: asio. https://github.com/ArashPartow/proxy/blob/master/tcpproxy_server.cpp

Tuttavia, lo scenario del mio caso è che i messaggi in entrata (dal lato client) sono veloci mentre in uscita (dal lato server) è lento per una sessione, per stare al passo con la velocità in entrata o per massimizzare il throughput totale del proxy, dobbiamo usare più sessioni sotto un'unica connessione.

Quindi questo framework I / O asincrono non funziona più. Abbiamo bisogno di un pool di thread da inviare al server assegnando a ogni thread una sessione.

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.