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 di I/O, il sistema crea un oggetto coda associato per i thread il cui scopo è quello di eseguire queste richieste. I processi che gestiscono molte richieste di I/O simultanee simultanee possono farlo in modo più rapido ed efficiente usando le porte di completamento di I/O insieme a un pool di thread pre-allocato rispetto alla creazione di thread al momento in cui ricevono una richiesta di I/O.

Funzionamento delle porte di completamento di I/O

La funzione CreateIoCompletionPort crea una porta di completamento di I/O e associa uno o più handle di file a tale porta. Quando viene completata un'operazione di I/O asincrona in uno di questi handle di file, un pacchetto di completamento di I/O viene accodato in coda nell'ordine fiFO (First-In-First-Out) alla porta di completamento I/O associata. Un uso potente 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, mentre i pacchetti vengono accodati nell'ordine FIFO, possono essere dequeued in un ordine diverso.

Nota

Il termine handle di file usato qui fa riferimento a un'astrazione del 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 pipe denominata o uno slot di posta elettronica. È possibile usare qualsiasi oggetto di sistema che supporta operazioni di I/O sovrapposte. Per un elenco di funzioni di I/O correlate, vedere la fine di questo argomento.

 

Quando un handle di 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 di I/O, anziché attendere direttamente il completamento dell'I/O asincrona. I thread che bloccano l'esecuzione in una porta di completamento di 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 rilasciato a un thread, il sistema rilascia l'ultimo thread (più recente) associato a tale porta, passando le informazioni di completamento per il completamento dell'I/O meno recente.

Anche se un numero qualsiasi di thread può chiamare GetQueuedCompletionStatus per una porta di completamento di 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 cose seguenti: il thread termina, specifica una porta di completamento I/O diversa oppure 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 di I/O, il sistema controlla prima il numero di thread associati a tale porta. 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 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 pacchetti di completamento in una coda di completamento di I/O. A tale scopo, 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 inviare notifiche ai thread di lavoro di eventi esterni, ad esempio.

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

Nota

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

 

Thread e concorrenza

La proprietà più importante di una porta di completamento di 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 runnable associati alla porta di completamento raggiunge il valore di concorrenza, il sistema blocca l'esecuzione di eventuali thread successivi associati a tale porta di completamento fino a quando il numero di thread runnable 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. Si consideri cosa accade con un valore di concorrenza di uno e più thread in attesa nella 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 blocca l'esecuzione perché, come accennato in precedenza, la coda del thread è LIFO. Invece, questo thread raccoglierà immediatamente il pacchetto di completamento in coda successivo. Nessun commutatori di contesto del thread si verificherà, perché il thread in esecuzione raccoglie continuamente pacchetti di completamento e gli altri thread non possono essere eseguiti.

Nota

Nell'esempio precedente, i thread aggiuntivi sembrano essere inutili e mai eseguiti, ma che presuppone che il thread in esecuzione non venga mai inserito in uno stato di attesa da parte di alcuni altri meccanismi, termina o in caso contrario chiude la porta di completamento di I/O associata. Prendere in considerazione tutte le ramificazioni di esecuzione del 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. 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 anche 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 di 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, potrebbe verificarsi un breve periodo quando 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 scende al di sotto del valore di concorrenza. Si tratta di un motivo per cui l'applicazione crea più thread nel pool di thread rispetto al valore di concorrenza. La gestione del pool di thread è oltre l'ambito di questo argomento, ma una buona regola di identificazione consiste nell'avere un minimo di due volte il numero massimo di thread nel pool di thread, come sono presenti processori nel sistema. Per altre 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 completate usando le porte di completamento di I/O. È necessario passare la funzione un'istanza della struttura OVERLAPPED e un handle di file precedentemente associato a una porta di completamento di I/O (da una chiamata a CreateIoCompletionPort) per abilitare il meccanismo di porta di completamento I/O:

Informazioni su processi e thread

BindIoCompletionCallback

CreateIoCompletionPort

GetQueuedCompletionStatus

GetQueuedCompletionStatusEx

PostQueuedCompletionStatus