2010-11-04 13 views
9

Ecco il codice:IAsyncResult.AsyncWaitHandle.WaitOne() completa prima del callback

class LongOp 
{ 
    //The delegate 
    Action longOpDelegate = LongOp.DoLongOp; 
    //The result 
    string longOpResult = null; 

    //The Main Method 
    public string CallLongOp() 
    { 
     //Call the asynchronous operation 
     IAsyncResult result = longOpDelegate.BeginInvoke(Callback, null); 

     //Wait for it to complete 
     result.AsyncWaitHandle.WaitOne(); 

     //return result saved in Callback 
     return longOpResult; 
    } 

    //The long operation 
    static void DoLongOp() 
    { 
     Thread.Sleep(5000); 
    } 

    //The Callback 
    void Callback(IAsyncResult result) 
    { 
     longOpResult = "Completed"; 
     this.longOpDelegate.EndInvoke(result); 
    } 
} 

Qui è il banco di prova:

[TestMethod] 
public void TestBeginInvoke() 
{ 
    var longOp = new LongOp(); 
    var result = longOp.CallLongOp(); 

    //This can fail 
    Assert.IsNotNull(result); 
} 

Se questo viene eseguito il test case può fallire. Perché esattamente?

C'è pochissima documentazione su come delegate.BeginInvoke funziona. Qualcuno ha qualche intuizione che vorrebbero condividere?

Aggiornamento Questa è una sottile condizione di gara che non è ben documentata in MSDN o altrove. Il problema, come spiegato nella risposta accettata, è che quando l'operazione viene completata, viene segnalata la Maniglia di attesa, quindi viene eseguita la richiamata. Il segnale rilascia il thread principale in attesa e ora l'esecuzione del callback entra nella "corsa". Jeffry Richter's suggested implementation mostra ciò che accade dietro le quinte:

// If the event exists, set it 
    if (m_AsyncWaitHandle != null) m_AsyncWaitHandle.Set(); 

    // If a callback method was set, call it 
    if (m_AsyncCallback != null) m_AsyncCallback(this); 

Per una soluzione riferimento alla risposta di Ben Voigt. Tale implementazione non comporta il sovraccarico aggiuntivo di un secondo handle di attesa.

+0

Rimuovere la richiamata e riprovare. – jgauffin

+0

@jgauffin, se noti che la domanda non sta chiedendo "Come faccio a far funzionare tutto questo?" Chiaramente questo è un esempio forzato. –

+0

La tua domanda è: "Se questo viene eseguito, il test case può fallire Perché esattamente?". Ho * risposto * a quello. Perché provi a mescolare due modi molto diversi di gestire un'operazione asincrona. – jgauffin

risposta

8

ASyncWaitHandle.WaitOne() viene segnalato quando l'operazione asincrona viene completata. Allo stesso tempo viene chiamato CallBack().

Ciò significa che il codice dopo WaitOne() viene eseguito nel thread principale e CallBack viene eseguito in un altro thread (probabilmente lo stesso che esegue DoLongOp()). Ciò si traduce in una condizione di competizione in cui il valore di longOpResult è essenzialmente sconosciuto al momento della restituzione.

Si potrebbe aspettare che ASyncWaitHandle.WaitOne() sarebbe stata segnalata quando il callback era finito, ma che non è solo come funziona ;-)

avrete bisogno di un altro ManualResetEvent avere il thread principale attendere che CallBack imposti longOpResult.

0

Il callback viene eseguito dopo il metodo CallLongOp. Poiché si imposta solo il valore della variabile nel callback, è ovvio che sarebbe null. Leggi questo: link text

+0

Vale a dire, il risultato che si sta cercando non è ancora stato impostato come callback non chiamato fino a dopo il ritorno del metodo CallLongOp. – Kell

+0

grazie per la tua risposta. Il callback non viene sempre eseguito dopo il metodo CallLongOp. Prova a inserire Thread.Sleep (500); in CallLongOp prima del ritorno longOpResult; e il test passerà. –

3

Quello che sta accadendo

Dal momento che il funzionamento DoLongOp ha completato, riprende il controllo all'interno CallLongOp e la funzione completa prima che l'operazione di richiamata è stata completata. Assert.IsNotNull(result); quindi eseguito prima dello longOpResult = "Completed";.

Perché? AsyncWaitHandle.WaitOne() sarà solo aspettare per l'operazione asincrona per completare, non il vostro richiamata

Il parametro di callback di BeginInvoke è in realtà un AsyncCallback delegate, che significa che il callback viene chiamata in modo asincrono. Questo è in base alla progettazione, poiché lo scopo è elaborare i risultati dell'operazione in modo asincrono (ed è l'intero scopo di questo parametro di callback).

Poiché la funzione BeginInvoke chiama effettivamente la funzione di richiamata, la chiamata IAsyncResult.WaitOne è solo per l'operazione e non influenza la richiamata.

Vedere Microsoft documentation (sezione Esecuzione di un metodo di richiamata al termine di una chiamata asincrona). C'è anche una buona spiegazione ed esempio.

Se il thread che avvia la chiamata asincrona non ha bisogno di essere il thread che elabora i risultati, è possibile eseguire un metodo di callback al termine della chiamata. Il metodo di callback viene eseguito su un thread ThreadPool.

Soluzione

Se si desidera attendere sia per il funzionamento e la richiamata, è necessario gestire la segnalazione da soli. A ManualReset è un modo per farlo che sicuramente ti dà il maggior controllo (ed è come Microsoft l'ha fatto nei loro documenti).

Qui viene modificato il codice utilizzando ManualResetEvent.

public class LongOp 
{ 
    //The delegate 
    Action longOpDelegate = LongOp.DoLongOp; 
    //The result 
    public string longOpResult = null; 

    // Declare a manual reset at module level so it can be 
    // handled from both your callback and your called method 
    ManualResetEvent waiter; 

    //The Main Method 
    public string CallLongOp() 
    { 
     // Set a manual reset which you can reset within your callback 
     waiter = new ManualResetEvent(false); 

     //Call the asynchronous operation 
     IAsyncResult result = longOpDelegate.BeginInvoke(Callback, null);  

     // Wait 
     waiter.WaitOne(); 

     //return result saved in Callback 
     return longOpResult; 
    } 

    //The long operation 
    static void DoLongOp() 
    { 
     Thread.Sleep(5000); 
    } 

    //The Callback 
    void Callback(IAsyncResult result) 
    { 
     longOpResult = "Completed"; 
     this.longOpDelegate.EndInvoke(result); 

     waiter.Set(); 
    } 
} 

Per l'esempio che hai dato, si sarebbe meglio non utilizzare un callback e invece gestire il risultato nella funzione CallLongOp, nel qual caso il vostro WaitOne sulla delegato operazione funzionerà bene.

+0

grazie per la risposta. Quindi, cosa facciamo esattamente con IAsyncResult che otteniamo da BeginInvoke()? –

+0

È possibile utilizzarlo per interrompere l'esecuzione nel metodo che ha chiamato begininvoke. Cioè Qualsiasi scenario in cui si desidera attendere l'operazione stessa da completare. – badbod99

+0

CONDIZIONI DI GARA! È preferibile creare l'evento prima di chiamare 'BeginInvoke', ma l'aggiunta di ulteriori oggetti di sincronizzazione non è necessaria e inefficiente. –

5

Come altri hanno già detto, result.WaitOne significa che l'obiettivo di BeginInvoke è terminato e non il callback. Quindi basta inserire il codice di post-elaborazione nel delegato BeginInvoke.

//Call the asynchronous operation 
    Action callAndProcess = delegate { longOpDelegate(); Callafter(); }; 
    IAsyncResult result = callAndProcess.BeginInvoke(r => callAndProcess.EndInvoke(r), null); 


    //Wait for it to complete 
    result.AsyncWaitHandle.WaitOne(); 

    //return result saved in Callafter 
    return longOpResult; 
+0

Ok ... davvero una bella soluzione! Ma per una spiegazione del perché penso di aver trattato abbastanza bene. – badbod99

+0

È intelligente ma quando sarebbe effettivamente utile? ManualReset ti dà il controllo di aspettare ogni volta che vuoi aspettare, questo chiama l'operazione quindi un callback per gestire il risultato e aspetta entrambi contemporaneamente. Potresti semplicemente gestire il risultato nell'operazione stessa se questo è ciò che volevi. – badbod99

+0

@ badbod99: questo ti consente di gestire il risultato anche se non hai scritto la funzione inserita in 'longOpDelegate' (o è un metodo di un'altra classe, e non ha accesso al membro 'longOpResult' privato, o tu non voglio introdurre l'accoppiamento inverso, o ...). –

0

Ho avuto lo stesso problema di recente, e ho pensato che un altro modo per risolverlo, ha funzionato nel mio caso. Bacialmente se il timeout non ti indebolisce, ricontrolla il flag IsCompleted quando Wait Handle è timeout. Nel mio caso, l'handle di attesa viene segnalato prima di bloccare il thread e subito dopo la condizione if, quindi ricontrollarlo dopo il timeout farà il trucco.

while (!AsyncResult.IsCompleted) 
{ 
    if (AsyncWaitHandle.WaitOne(10000)) 
     break; 
} 
Problemi correlati