Il presente articolo è stato tradotto automaticamente.

Windows con C++

Timer di pool di thread e I/O

Kenny Kerr

Kenny KerrIn questo, il mio capitolo finale sul pool di thread di Windows 7, ho intenzione di coprire i due rimanenti oggetti generano callback forniti dall'API. C'è persino più potrei scrivere circa il pool di thread, ma dopo cinque articoli che coprono praticamente tutte le sue caratteristiche, si dovrebbe essere comodo utilizzarlo per le applicazioni di potenza, efficace ed efficiente.

Nel mio mese di agosto (msdn.microsoft.com/magazine/hh335066) e novembre (msdn.microsoft.com/magazine/hh547107) colonne, ho descritto lavoro e attendere gli oggetti rispettivamente. Un oggetto di lavoro consente di presentare il lavoro, sotto forma di una funzione, direttamente al pool di thread per l'esecuzione. La funzione eseguirà il più presto possibile. Un oggetto di attesa racconta il pool di thread di aspettare per un oggetto di sincronizzazione del kernel per vostro conto e una funzione in coda quando esso viene segnalato. Questa è un'alternativa scalabile a primitive di sincronizzazione tradizionale e un'efficace alternativa al polling. Ci sono molti casi in cui il timer sono necessari per l'esecuzione del codice dopo un certo intervallo o in un certo regolare. Questo potrebbe essere causa di una mancanza di sostegno "push" in qualche protocollo Web o forse perché si sta implementando un protocollo di comunicazione UDP-stile ed è necessario gestire ritrasmissioni. Fortunatamente, il pool di thread API fornisce un oggetto timer a tutti questi scenari di gestire in maniera efficiente e ora nota.

Oggetti timer

La funzione CreateThreadpoolTimer crea un oggetto timer. Se la funzione ha esito positivo, restituisce un puntatore opaco che rappresenta l'oggetto timer. Se non riesce, e restituisce un valore di puntatore null e fornisce ulteriori informazioni tramite la funzione GetLastError. Dato un oggetto timer, la funzione di CloseThreadpoolTimer informa il pool di thread che l'oggetto può essere rilasciato. Se hai seguito della serie, questo dovrebbe tutti suono molto familiare. Qui è una classe di tratti che può essere utilizzata con il modello di classe pratico unique_handle ho introdotto nel mio articolo di luglio 2011 (msdn.microsoft.com/magazine/hh288076):

struct timer_traits
{
  static PTP_TIMER invalid() throw()
  {
    return nullptr;
  }
  static void close(PTP_TIMER value) throw()
  {
    CloseThreadpoolTimer(value);
  }
};
typedef unique_handle<PTP_TIMER, timer_traits> timer;

Ora posso utilizzare il typedef e creare un oggetto timer come segue:

void * context = ...
timer t(CreateThreadpoolTimer(its_time, context, nullptr));
check_bool(t);

Come al solito, il parametro finale facoltativamente accetta un puntatore ad un ambiente in modo è possibile associare l'oggetto timer con un ambiente, come descritto nel mio articolo di settembre 2011 (msdn.microsoft.com/­rivista/hh416747). Il primo parametro è la funzione di callback che sarà essere accodata al pool di thread ogni volta che il timer scade. Il callback di timer viene dichiarato come segue:

void CALLBACK its_time(PTP_CALLBACK_INSTANCE, void * context, PTP_TIMER);

Per controllare quando e quanto spesso il timer scade, utilizzare la funzione SetThreadpoolTimer. Naturalmente, il primo parametro fornisce l'oggetto timer, ma il secondo parametro indica il tempo debito alla quale deve scadere il timer. Utilizza una struttura FILETIME per descrivere il tempo assoluto o relativo. Se non siete abbastanza sicuro di come funziona, vi incoraggio a leggere l'articolo del mese scorso, dove ho descritto la semantica attorno alla struttura FILETIME in dettaglio. Ecco un semplice esempio dove impostare il timer per scadere in cinque secondi:

union FILETIME64
{
  INT64 quad;
  FILETIME ft;
};
FILETIME relative_time(DWORD milliseconds)
{
  FILETIME64 ft = { -static_cast<INT64>(milliseconds) * 10000 };
  return ft.ft;
}
auto due_time = relative_time(5 * 1000);
SetThreadpoolTimer(t.get(), &due_time, 0, 0);

Ancora una volta, se non siete sicuri su come funziona la funzione di relative_time, per favore leggere la mia colonna novembre 2011. In questo esempio, il timer scadrà dopo cinque secondi, a quel punto il pool di thread accoderà un'istanza della funzione di callback its_time. A meno che non si interviene, nessun ulteriore callback sarà in coda.

È anche possibile utilizzare SetThreadpoolTimer per creare un timer periodico che accoderà un callback su alcuni intervalli regolari. Ecco un esempio:

auto due_time = relative_time(5 * 1000);
SetThreadpoolTimer(t.get(), &due_time, 500, 0);

In questo esempio, richiamata del timer in primo luogo viene accodata dopo cinque secondi e quindi ogni mezzo secondo dopo che, fino a quando l'oggetto timer è reimpostare o chiuso. A differenza di tempo debito, il periodo è semplicemente specificato in millisecondi. Tenere presente che un timer periodico accoderà un callback dato periodo trascorso, indipendentemente da quanto tempo ci vuole il callback da eseguire. Questo significa che è possibile che più callback da eseguire contemporaneamente o sovrapposizione, se l'intervallo è abbastanza piccolo o i callback di prendono un abbastanza a lungo tempo di esecuzione.

Se è necessario garantire callback non si sovrappongano e l'ora di inizio preciso per ogni periodo non è che importante, quindi un approccio diverso per la creazione di un timer periodico potrebbe essere appropriato. Anziché specificare un periodo nella chiamata a SetThreadpoolTimer, semplicemente reimpostare il timer nel callback stessa. In questo modo, è possibile assicurare che le richiamate non si sovrapporranno mai. Se non altro, ciò semplifica il debug. Immaginate di fare un passo attraverso un callback di timer nel debugger solo per scoprire che il pool di thread già in coda un qualche più istanze, mentre erano analizzando il codice (o ricarica il tuo caffè). Con questo approccio, che non accadrà mai. Ecco come appare:

void CALLBACK its_time(PTP_CALLBACK_INSTANCE, void *, PTP_TIMER timer)
{
  // Your code goes here
  auto due_time = relative_time(500);
  SetThreadpoolTimer(timer, &due_time, 0, 0);
}
auto due_time = relative_time(5 * 1000);
SetThreadpoolTimer(t.get(), &due_time, 0, 0);

Come potete vedere, l'iniziale dovuto tempo è di cinque secondi e poi a reimpostare il tempo debito a 500 ms all'estremità del callback. Ho preso vantaggio dal fatto che la firma di callback fornisce un puntatore all'oggetto timer originari, rendendo il lavoro di reimpostazione del timer molto semplice. Si consiglia inoltre di utilizzare RAII per assicurare che la chiamata a SetThreadpoolTimer in modo affidabile viene chiamata prima il callback restituisce.

È possibile chiamare SetThreadpoolTimer con un valore di puntatore null per il tempo debito fermare qualsiasi scadenze future timer che possono provocare ulteriori callback. Inoltre sarà necessario chiamare WaitForThreadpool­TimerCallbacks per evitare qualsiasi gara condizioni. Naturalmente, gli oggetti timer funzionano altrettanto bene con gruppi di pulitura, come descritto nel mio articolo di ottobre 2011.

Il parametro finale di SetThreadpoolTimer può essere un po' di confusione, perché la documentazione si riferisce ad una "lunghezza della finestra" come pure un ritardo. Che cosa è che tutto su? Questo è in realtà una funzione che colpisce l'efficienza energetica e aiuta a ridurre la potenza complessiva consumo. Si basa su una tecnica chiamata timer coalescenza. Ovviamente, la soluzione migliore è quello di evitare del tutto il timer e utilizzare gli eventi invece. Questo permette di processori del sistema la maggior quantità di tempo di inattività, in tal modo, incoraggiandoli a entrare loro basso consumo negli Stati inattivo quanto più possibile. Ancora, se timer sono necessari, timer coalescenza può ridurre il complessivo consumo energetico, riducendo il numero di interrupt timer che sono richiesti. Timer coalescenza è basato sull'idea di un "ritardo tollerabile" per le scadenze del timer. Dato certo ritardo tollerabile, il kernel di Windows può regolare l'ora di scadenza effettivo in concomitanza con qualsiasi timer esistenti. Una buona regola è di impostare il ritardo su un decimo del periodo in uso. Ad esempio, se il timer deve scadere in 10 secondi, utilizzare un ritardo di un secondo, a seconda di ciò che è appropriato per la vostra applicazione. Maggiore è il ritardo, interrompe l'opportunità più che il kernel ha per ottimizzare il timer. D'altra parte, niente di meno di 50 ms non sarà di grande utilità perché comincia a invadere intervallo di orologio del kernel predefinito.

Oggetti di completamento dei / O

Ora è il momento per me, per introdurre la gemma del pool di thread API: l'oggetto di completamento di input/output (i/O), o semplicemente l'oggetto dei / O. Torna quando introdotto il pool di thread API, ho detto che il pool di thread è costruito sulla cima dell'I/O completamento porta API. Tradizionalmente, l'implementazione dei / O più scalabile su Windows era possibile solo utilizzando l'API di porta di completamento di I/O. Ho scritto su questa API in passato. Anche se non particolarmente difficile da usare, non era sempre che facile da integrare con un'applicazione altri threading necessita. Grazie per l'API di pool di thread, però, avete il meglio dei due mondi con una sola API per il lavoro, sincronizzazione, timer e ora i/O, troppo. L'altro vantaggio è che il completamento dei / O sovrapposta con il pool di thread di esecuzione è effettivamente più intuitiva rispetto all'utilizzo dell'API di porto I/O completamento, soprattutto quando si tratta di manipolazione di handle di file multipli e più sovrapposte operazioni contemporaneamente.

Come si può immaginare, la funzione CreateThreadpoolIo crea un oggetto dei / O e la funzione di CloseThreadpoolIo informa il pool di thread che l'oggetto può essere rilasciato. Qui è una classe di tratti per il modello di classe unique_handle:

struct io_traits
{
  static PTP_IO invalid() throw()
  {
    return nullptr;
  }
  static void close(PTP_IO value) throw()
  {
    CloseThreadpoolIo(value);
  }
};
typedef unique_handle<PTP_IO, io_traits> io;

La funzione CreateThreadpoolIo accetta un handle di file, che implica che un oggetto dei / O è in grado di controllare il / O per un singolo oggetto. Naturalmente, che l'oggetto deve supportare i/O sovrapposte, ma sono inclusi i tipi di risorsa popolare come ad esempio i file di sistema di file, named pipe, socket e così via. Mi permetta di dimostrare con un semplice esempio di in attesa di ricevere un pacchetto UDP utilizza un socket. Per gestire il socket, utilizzerò unique_handle con la seguente classe tratti:

struct socket_traits
{
  static SOCKET invalid() throw()
  {
    return INVALID_SOCKET;
  }
  static void close(SOCKET value) throw()
  {
    closesocket(value);
  }
};
typedef unique_handle<SOCKET, socket_traits> socket;

A differenza delle classi di tratti che vi ho mostrato finora, in questo caso la funzione non valida non restituisce un valore di puntatore null. Questo è perché la funzione di WSASocket, come la funzione CreateFile, utilizza un valore insolito per indicare un handle non valido. Data questa classe di tratti e typedef, posso creare una presa di corrente e l'oggetto i/O semplicemente:

socket s(WSASocket( ...
, WSA_FLAG_OVERLAPPED));
check_bool(s);
void * context = ...
io i(CreateThreadpoolIo(reinterpret_cast<HANDLE>(s.get()), io_completion, context, nullptr));
check_bool(i);

La funzione di callback che segnala il completamento di qualsiasi operazione di I/O viene dichiarata come segue:

void CALLBACK io_completion(PTP_CALLBACK_INSTANCE, void * context, void * overlapped,
  ULONG result, ULONG_PTR bytes_copied, PTP_IO)

I parametri univoci per questo callback dovrebbero essere familiari se avete usato i/O sovrapposte prima. Poiché i/O sovrapposte è per natura asincrona e permette la sovrapposizione di operazioni i/O — da qui il nome sovrapposto I / O — ci deve essere un modo per identificare l'operazione I/O particolare che è stata completata. Questo è lo scopo del parametro sovrapposto. Questo parametro fornisce un puntatore alla struttura OVERLAPPED o WSAOVERLAPPED che è stato specificato quando in primo luogo è stata avviata in una determinata operazione di I/O. L'approccio tradizionale di una struttura OVERLAPPED di imballaggio in una struttura più grande per appendere più dati fuori questo parametro può essere ancora utilizzato. Il parametro sovrapposto fornisce un modo per identificare l'operazione I/O particolare che è stata completata, mentre il parametro context — come al solito — fornisce un contesto per l'endpoint I/O, indipendentemente dal qualsiasi operazione particolare. Tenuto conto di questi due parametri, si dovrebbe avere alcuna difficoltà a coordinare il flusso di dati attraverso l'applicazione. Il parametro risultato ti dice se l'operazione sovrapposta è riuscito con la solita ERROR_SUCCESS, o zero, che indica il successo. Infine, il parametro bytes_copied ovviamente ti dice quanti byte sono stati effettivamente letto o scritti. Un errore comune è presumere che il numero di byte richiesto è stato effettivamente copiato. Non fare quell'errore: è la ragione per l'esistenza di questo parametro.

L'unica parte del supporto dei / O del pool di thread che è un po' complicata è la manipolazione della richiesta dei / O se stesso. Si prende cura di questo codice correttamente. Prima di chiamare una funzione per avviare qualche operazione asincrona di I/O, ad esempio ReadFile o WSARecvFrom, è necessario chiamare la funzione StartThreadpoolIo per far sapere che un'operazione I/O sta per iniziare a pool di thread. Il trucco è che se l'operazione di I/O accade per completare in modo sincrono, allora deve notificare il pool di thread di questo chiamando la funzione CancelThreadpoolIo. Tieni presente che non necessariamente equivale il completamento i/O di completamento. Un'operazione di I/O potrebbe riuscire o fallire entrambi in modo sincrono o asincrono. Ad ogni modo, se l'operazione di I/O non notificherà la porta di completamento della funzionalità di completamento, è necessario informare il pool di thread. Ecco che cosa questo potrebbe apparire come nel contesto della ricezione di un pacchetto UDP:

StartThreadpoolIo(i.get());
auto result = WSARecvFrom(s.get(), ...
if (!result)
{
  result = WSA_IO_PENDING;
}
else
{
  result = WSAGetLastError();
}
if (WSA_IO_PENDING != result)
{
  CancelThreadpoolIo(i.get());
}

Come potete vedere, comincio il processo chiamando StartThreadpoolIo per indicare al pool di thread che un'operazione I/O sta per iniziare. Invito quindi WSARecvFrom per ottenere le cose da fare. Interpretare il risultato è la parte cruciale. La funzione WSARecvFrom restituisce zero se l'operazione completata con successo, ma la porta di completamento verrà ancora notificato, quindi cambiare il risultato al WSA_IO_PENDING. Qualsiasi altro risultato da WSARecvFrom indica il fallimento, con l'eccezione, naturalmente, di WSA_IO_PENDING, che significa semplicemente che l'operazione è stata avviata con successo, ma dovranno essere completato successivamente. Ora, semplicemente chiamare CancelThreadpoolIo se il risultato non è in sospeso per mantenere il pool di thread fino a velocità. Diversi endpoint I/O può fornire la semantica diversa. Ad esempio, file i/O può essere configurato per evitare di avvisare la porta di completamento il completamento sincrono. È quindi necessario chiamare CancelThreadpoolIo come appropriato.

Come gli altri generano callback di oggetti nel pool di thread API, richiamate per i/O in attesa oggetti possono essere annullati utilizzando la funzione WaitForThreadpoolIoCallbacks. Basta tenere a mente che questo sarà annullare qualsiasi callback in sospeso, ma non cancellare qualsiasi in sospeso di operazioni dei / O se stessi. Sarà ancora necessario utilizzare la funzione appropriata per annullare l'operazione per evitare eventuali condizioni di gara. Questo consente di in modo sicuro senza alcuna struttura OVERLAPPED e così via.

E questo è tutto per il pool di thread API. Come ho detto, non c'è più potrei scrivere su questa API potente, ma dato il Walk-through dettagliate ho fornito finora, sono sicuro che siete sulla buona strada per usarlo per alimentare la vostra domanda successiva. Unisciti a me il mese prossimo come continuo a esplorare Windows con C++.

Kenny Kerr è un artigiano del software con la passione per lo sviluppo di Windows nativi. Contattarlo al kennykerr.ca.