2012-05-24 10 views
7

Sto eseguendo alcuni I/O di rete asincroni utilizzando i metodi di stile Begin/End. (In realtà è una query su Azure Table Storage, ma non penso che importi.) Ho implementato un timeout lato client usando lo ThreadPool.RegisterWaitForSingleObject(). Funziona bene per quanto posso dire.C#: utilizzo di RegisterWaitForSingleObject se l'operazione viene completata per la prima volta

Perché ThreadPool.RegisterWaitForSingleObject() prende un WaitHandle come argomento, devo iniziare l'operazione di I/O, quindi eseguire ThreadPool.RegisterWaitForSingleObject(). Sembra che questo introduca la possibilità che l'I/O completi prima ancora di registrare l'attesa.

Un esempio di codice semplificato:

private void RunQuery(QueryState queryState) 
{ 
    //Start I/O operation 
    IAsyncResult asyncResult = queryState.Query.BeginExecuteSegmented(NoopAsyncCallback, queryState); 

    //What if the I/O operation completes here? 

    queryState.TimeoutWaitHandle = ThreadPool.RegisterWaitForSingleObject(asyncResult.AsyncWaitHandle, QuerySegmentCompleted, asyncResult, queryTimeout, true); 
} 

private void QuerySegmentCompleted(object opState, bool timedOut){ 
    IAsyncResult asyncResult = opState as IAsyncResult; 
    QueryState state = asyncResult.AsyncState as QueryState; 

    //If the I/O completed quickly, could TimeoutWaitHandle could be null here? 
    //If so, what do I do about that? 
    state.TimeoutWaitHandle.Unregister(asyncResult.AsyncWaitHandle); 
} 

Qual è il modo corretto di gestire questa situazione? Devo ancora preoccuparmi di Unregister() di AsyncWaitHandle? Se è così, c'è un modo abbastanza semplice per aspettare che venga impostato?

+0

Avete provato a inserire un 'Thread.Sleep' nel mezzo per consentire il completamento dell'operazione di I/O e vedere cosa succede? – mellamokb

+0

Non ho. Penso di averlo visto accadere solo forse 3-4 volte, e poi solo su macchine di produzione pesantemente caricate. Preferisco non iniziare ad aggiungere le chiamate Sleep() al mio vero codice. –

+0

Provalo nel tuo ambiente di sviluppo, ovviamente. Quando dici che hai visto accadere solo 3-4 volte, che cosa hai visto accadere? Una NullPointerException casuale e inspiegabile? – mellamokb

risposta

4

Sì, tu e tutti gli altri hanno questo problema. E non importa se l'IO completato in modo sincrono o meno. C'è ancora una corsa tra il callback e il compito. Microsoft avrebbe dovuto fornire automaticamente la funzione di callback allo RegisteredWaitHandle. Questo avrebbe risolto tutto. Oh bene, il senno di poi è sempre 20-20 come si suol dire.

Quello che devi fare è continuare a leggere la variabile RegisteredWaitHandle fino a quando non è più nullo. Va bene farlo in un circuito chiuso perché la gara è abbastanza sottile che il circuito non girerà molte volte.

private void RunQuery(QueryState queryState) 
{ 
    // Start the operation. 
    var asyncResult = queryState.Query.BeginExecuteSegmented(NoopAsyncCallback, queryState); 

    // Register a callback. 
    RegisteredWaitHandle shared = null; 
    RegisteredWaitHandle produced = ThreadPool.RegisterWaitForSingleObject(asyncResult.AsyncWaitHandle, 
    (state, timedout) => 
    { 
     var asyncResult = opState as IAsyncResult; 
     var state = asyncResult.AsyncState as QueryState; 
     while (true) 
     { 
     // Keep reading until the value is no longer null. 
     RegisteredWaitHandle consumed = Interlocked.CompareExchange(ref shared, null, null); 
     if (consumed != null) 
     { 
      consumed.Unregister(asyncResult.AsyncWaitHandle); 
      break; 
     } 
     } 
    }, asyncResult, queryTimeout, true); 

    // Publish the RegisteredWaitHandle so that the callback can see it. 
    Interlocked.CompareExchange(ref shared, produced, null); 
} 
+0

Penso di vedere cosa stai guidando qui. È davvero necessario avere le due variabili RegisteredWaitHandle e CompareExchange()? Poiché l'assegnazione del puntatore è un'operazione atomica, non posso semplicemente esaminare la variabile 'produced' all'interno del callback? –

+0

@breischl: Beh, hai bisogno di una barriera di memoria in modo da ottenere una "nuova lettura" della variabile su ogni iterazione. Probabilmente starai bene ad omettere la chiamata 'Interlocked.CompareExchange' perché sono sicuro che 'RegisteredWaitHandle.Unregistered' genera anche una barriera di memoria. Ma, personalmente, rimango con questo modello perché "Interlocked.CompareExchange" lo rende esplicito. –

+0

Non dichiarare che 'produced' come' volatile' ha lo stesso effetto? –

1

Non è necessario annullare la registrazione se l'I/O è stato completato prima del timeout poiché era il completamento che ha segnalato la richiamata. Infatti, leggendo i documenti del metodo Unregister, sembra del tutto inutile chiamarlo mentre esegui una sola volta e non stai annullando la registrazione in un metodo non correlato.

http://msdn.microsoft.com/en-us/library/system.threading.registeredwaithandle.unregister.aspx

Se un metodo callback è in corso quando Deregistra esegue, waitObject non viene segnalata finché il metodo callback completa. In particolare, se un metodo di callback esegue l'annullamento della registrazione, waitObject non viene segnalato fino al completamento del metodo di callback.

+0

Penso che la documentazione citata sta dicendo che non segnalerà WaitHandle della mia operazione. Non sembra dire nulla riguardo a RegisteredWaitHandle che ho ricevuto dal ThreadPool. Ma è terribilmente confuso, quindi potrei sbagliarmi. La documentazione su RegisterWaitForSingleObject() dice esplicitamente che devi sempre annullare la registrazione(), quindi penso di aver bisogno di farlo da qualche parte. http://msdn.microsoft.com/en-us/library/w9f75h7a.aspx –

+0

@breischl è QuerySegmentCompleted chiamato solo come richiamata da RegisterWaitForSingleObject? – Slugart

+0

Attualmente sì. –

Problemi correlati