Porte di completamento di I/O

Le porte di completamento di I/O forniscono un modello di threading efficiente per l'elaborazione di più richieste di I/O asincrone in un sistema multiprocessore. Quando un processo crea una porta di completamento I/O, il sistema crea un oggetto coda associato per i thread il cui unico scopo è quello di gestire queste richieste. I processi che gestiscono molte richieste di I/O asincrone simultanee possono farlo in modo più rapido ed efficiente usando le porte di completamento di I/O in combinazione con un pool di thread preallocato rispetto alla creazione di thread al momento in cui ricevono una richiesta di I/O.

Quando usare le porte di completamento di I/O

Scenario Approccio consigliato
Server ad alte prestazioni che gestisce centinaia/migliaia di connessioni simultanee Porte di completamento di I/O , progettate appositamente per questo. Il kernel gestisce la pianificazione dei thread in modo che corrisponda alla concorrenza della CPU.
Concorrenza moderata (decine di operazioni asincrone) I/O del pool di thread :CreateThreadpoolIo API più semplice, gestisce internamente IOCP. Da preferire per il codice nuovo che non richiede la gestione manuale dei thread.
Semplici operazioni asincrone dei file in C++ moderno Coroutine C++20 con un dispatcher IOCP personalizzato o .NET FileStream con async/await.
I/O a thread singolo o a basso volume I/O sincrono o I/O semplice sovrapposto con segnalazione degli eventi. IOCP aggiunge complessità non necessaria per gli scenari a flusso singolo.

Nota

API del pool di thread vs IOCP grezzo: L'API del pool di thread di Windows (CreateThreadpoolIo, StartThreadpoolIo) utilizza internamente IOCP, ma gestisce automaticamente il ciclo di vita dei thread. Per le nuove applicazioni server, considerare prima l'API del pool di thread, che offre la stessa scalabilità con meno codice boilerplate. Usare IOCP non elaborato quando è necessario un controllo esplicito sul valore di concorrenza della porta di completamento o sulla gestione dei thread personalizzati.

Funzionamento delle porte di completamento di I/O

La funzione CreateIoCompletionPort crea una porta di completamento I/O e associa uno o più handle di file a tale porta. Quando viene completata un'operazione di I/O asincrona su uno di questi handle di file, un pacchetto di completamento di I/O viene accodato in ordine FIFO (First-In-First-Out) alla porta di completamento I/O associata. Un uso efficace per questo meccanismo consiste nel combinare il punto di sincronizzazione per più handle di file in un singolo oggetto, anche se sono presenti anche altre applicazioni utili. Si noti che, sebbene i pacchetti vengano accodati nell'ordine FIFO, potrebbero essere rimossi dalla coda in un ordine diverso.

Nota

Il termine handle di file usato qui fa riferimento a un'astrazione di sistema che rappresenta un endpoint di I/O sovrapposto, non solo un file su disco. Ad esempio, può essere un endpoint di rete, un socket TCP, una named pipe o uno slot di posta elettronica. È possibile usare qualsiasi oggetto di sistema che supporta operazioni di I/O sovrapposte. Per un elenco delle funzioni di I/O correlate, vedere la fine di questo argomento.

Quando un handle del file è associato a una porta di completamento, il blocco di stato passato non verrà aggiornato finché il pacchetto non viene rimosso dalla porta di completamento. L'unica eccezione è se l'operazione originale restituisce in modo sincrono un errore. Un thread (uno creato dal thread principale o il thread principale stesso) usa la funzione GetQueuedCompletionStatus per attendere che un pacchetto di completamento venga accodato alla porta di completamento I/O, anziché attendere il completamento diretto dell'I/O asincrono. I thread che bloccano l'esecuzione su una porta di completamento I/O vengono rilasciati nell'ordine LIFO (Last-In-First-Out) e il pacchetto di completamento successivo viene estratto dalla coda FIFO della porta di completamento I/O per tale thread. Ciò significa che, quando un pacchetto di completamento viene assegnato a un thread, il sistema attiva l'ultimo thread (più recente) associato a tale porta, trasmettendogli le informazioni di completamento relative al completamento di I/O meno recente.

Anche se un numero qualsiasi di thread può chiamare GetQueuedCompletionStatus per una porta di completamento I/O specificata, quando un thread specificato chiama GetQueuedCompletionStatus la prima volta, diventa associato alla porta di completamento I/O specificata fino a quando non si verifica una delle tre operazioni seguenti: il thread viene chiuso, specifica una porta di completamento I/O diversa o chiude la porta di completamento I/O specificata. In altre parole, un singolo thread può essere associato al massimo a una porta di completamento di I/O.

Quando un pacchetto di completamento viene accodato a una porta di completamento I/O, il sistema controlla innanzitutto quanti thread associati a tale porta sono in esecuzione. Se il numero di thread in esecuzione è minore del valore di concorrenza (illustrato nella sezione successiva), uno dei thread in attesa (quello più recente) può elaborare il pacchetto di completamento. Quando un thread in esecuzione completa l'elaborazione, in genere chiama nuovamente GetQueuedCompletionStatus , a quel punto restituisce con il pacchetto di completamento successivo o attende se la coda è vuota.

I thread possono usare la funzione PostQueuedCompletionStatus per inserire i pacchetti di completamento nella coda della porta di completamento I/O. In questo modo, la porta di completamento può essere usata per ricevere comunicazioni da altri thread del processo, oltre a ricevere pacchetti di completamento di I/O dal sistema di I/O. La funzione PostQueuedCompletionStatus consente a un'applicazione di accodare i propri pacchetti di completamento speciali alla porta di completamento di I/O senza avviare un'operazione di I/O asincrona. Ciò è utile per notificare ai thread di lavoro eventi esterni, ad esempio.

L'handle di porta di completamento I/O e ogni handle di file associato a tale porta di completamento I/O specifico sono noti come riferimenti alla porta di completamento I/O. La porta di completamento I/O viene rilasciata quando non vi sono più riferimenti ad essa. Pertanto, tutti questi handle devono essere chiusi correttamente per rilasciare la porta di completamento I/O e le risorse di sistema associate. Dopo aver soddisfatto queste condizioni, un'applicazione deve chiudere l'handle della porta di completamento I/O chiamando la funzione CloseHandle .

Nota

Una porta di completamento I/O è associata al processo che lo ha creato e non è condivisibile tra i processi. Tuttavia, un singolo handle è condivisibile tra thread nello stesso processo.

Thread e concorrenza

La proprietà più importante di una porta di completamento I/O da considerare attentamente è il valore di concorrenza. Il valore di concorrenza di una porta di completamento viene specificato quando viene creato con CreateIoCompletionPort tramite il parametro NumberOfConcurrentThreads . Questo valore limita il numero di thread eseguibili associati alla porta di completamento. Quando il numero totale di thread eseguibili associati alla porta di completamento raggiunge il valore di concorrenza, il sistema blocca l'esecuzione di tutti i thread successivi associati a tale porta di completamento fino a quando il numero di thread eseguibili scende al di sotto del valore di concorrenza.

Lo scenario più efficiente si verifica quando sono presenti pacchetti di completamento in attesa nella coda, ma non è possibile soddisfare attese perché la porta ha raggiunto il limite di concorrenza. Pensare a ciò che accade con un valore di concorrenza pari a uno e diversi thread in attesa della chiamata alla funzione GetQueuedCompletionStatus. In questo caso, se la coda ha sempre pacchetti di completamento in attesa, quando il thread in esecuzione chiama GetQueuedCompletionStatus, non bloccherà l'esecuzione perché, come accennato in precedenza, la coda del thread è LIFO. Invece, questo thread preleverà immediatamente il prossimo pacchetto di completamento in coda. Non si verificheranno cambi di contesto del thread, perché il thread in esecuzione raccoglie continuamente pacchetti di completamento e gli altri thread non sono in grado di eseguire.

Nota

Nell'esempio precedente, i thread aggiuntivi sembrano inutili e come se non venissero mai eseguiti, ma ciò presuppone che il thread in esecuzione non venga mai messo in stato di attesa da qualche altro meccanismo, non termini e non chiuda in altro modo la porta di completamento I/O associata. Prendere in considerazione tutte queste ramificazioni di esecuzione di thread durante la progettazione dell'applicazione.

Il valore massimo complessivo migliore da selezionare per il valore di concorrenza è il numero di CPU nel computer. Se la transazione richiede un calcolo lungo, un valore di concorrenza più grande consentirà l'esecuzione di più thread. Il completamento di ogni pacchetto di completamento può richiedere più tempo, ma più pacchetti di completamento verranno elaborati contemporaneamente. È possibile sperimentare il valore di concorrenza insieme agli strumenti di profilatura per ottenere l'effetto migliore per l'applicazione.

Il sistema consente inoltre a un thread in attesa in GetQueuedCompletionStatus di elaborare un pacchetto di completamento se un altro thread in esecuzione associato alla stessa porta di completamento I/O entra in uno stato di attesa per altri motivi, ad esempio la funzione SuspendThread . Quando il thread nello stato di attesa inizia di nuovo l'esecuzione, può verificarsi un breve periodo in cui il numero di thread attivi supera il valore di concorrenza. Tuttavia, il sistema riduce rapidamente questo numero non consentendo nuovi thread attivi fino a quando il numero di thread attivi non scende al di sotto del valore di concorrenza. Questo è un motivo per cui l'applicazione crea più thread nel pool di thread rispetto al valore di concorrenza. La gestione del pool di thread esula dall'ambito di questo argomento, ma una buona regola generale è avere nel pool di thread almeno un numero di thread pari al doppio del numero di processori presenti nel sistema. Per ulteriori informazioni sul pool di thread, vedere Pool di thread.

Funzioni di I/O supportate

Le funzioni seguenti possono essere usate per avviare operazioni di I/O che vengono completate tramite le porte di completamento I/O. È necessario passare alla funzione un'istanza della struttura OVERLAPPED e un handle di file associato in precedenza a una porta di completamento di I/O (tramite una chiamata a CreateIoCompletionPort) per abilitare il meccanismo di porta di completamento di I/O:

Informazioni su processi e thread

BindIoCompletionCallback

CreateIoCompletionPort