2010-11-04 17 views
9

Hier ist der Code:IAsyncResult.AsyncWaitHandle.WaitOne() vervollständigt vor Rückruf

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); 
    } 
} 

Hier ist der Testfall:

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

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

Wenn dies der Testfall versagen kann ausgeführt wird. Warum genau?

Es gibt sehr wenig Dokumentation darüber, wie delegate.BeginInvoke funktioniert. Hat jemand irgendwelche Einsichten, die sie teilen möchten?

Aktualisieren Dies ist eine subtile Race-Bedingung, die in MSDN oder anderswo nicht gut dokumentiert ist. Das Problem, wie in der akzeptierten Antwort erklärt, ist, dass, wenn die Operation abgeschlossen ist, der Wait-Handle signalisiert wird, und dann der Callback ausgeführt wird. Das Signal gibt den wartenden Haupt-Thread frei und nun tritt die Callback-Ausführung in das "Rennen" ein. Jeffry Richter's suggested implementation zeigt, was hinter den Kulissen passiert:

// 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); 

Für eine Lösung zu Ben Voigt Antwort verweisen. Diese Implementierung verursacht nicht den zusätzlichen Aufwand eines zweiten Warte-Handle.

+0

Entfernen Sie den Rückruf und versuchen Sie es erneut. – jgauffin

+0

@jgauffin, wenn Sie bemerken, dass die Frage nicht "Wie bekomme ich das zur Arbeit?" Dies ist eindeutig ein konstruiertes Beispiel. –

+0

Ihre Frage ist: "Wenn dies ausgeführt wird, kann der Testfall fehlschlagen. Warum genau?". Ich habe * das * beantwortet. Weil Sie versuchen, zwei sehr unterschiedliche Arten der Handhabung einer asynchronen Operation zu mischen. – jgauffin

Antwort

8

Die ASyncWaitHandle.WaitOne() wird signalisiert, wenn der asynchrone Vorgang abgeschlossen ist. Gleichzeitig wird CallBack() aufgerufen.

Das bedeutet, dass der Code nach WaitOne() im Hauptthread ausgeführt wird und der CallBack in einem anderen Thread ausgeführt wird (wahrscheinlich derselbe, der DoLongOp() ausführt). Dies führt zu einer Race-Bedingung, bei der der Wert von longOpResult zum Zeitpunkt der Rückgabe im Wesentlichen unbekannt ist.

Man hätte erwartet, dass ASyncWaitHandle.WaitOne() signalisiert worden wäre, wenn der Rückruf beendet war, aber das ist einfach nicht, wie es ;-)

arbeitet Sie einen anderen Manual benötigen den Haupt-Thread haben Warten Sie, bis CallBack longOpResult einstellt.

0

Der Rückruf wird nach der CallLongOp-Methode ausgeführt. Da Sie den Variablenwert nur im Rückruf festgelegt haben, liegt es nahe, dass er null ist. Lesen Sie: link text

+0

Das Ergebnis, nach dem Sie suchen, wurde noch nicht als Callback festgelegt und wird erst aufgerufen, nachdem die CallLongOp-Methode zurückgegeben wurde. – Kell

+0

danke für deine antwort. Der Callback wird nicht immer nach der CallLongOp-Methode ausgeführt. Versuchen Sie Thread.Sleep (500) zu setzen; in CallLongOp vor Rückgabe longOpResult; und der Test wird bestanden. –

3

Was

Da Ihr Betrieb geschieht DoLongOp abgeschlossen, Kontrolle fortgesetzt innerhalb CallLongOp und die Funktion abgeschlossen ist, bevor der Rückruf-Operation abgeschlossen ist. Assert.IsNotNull(result); wird dann vor longOpResult = "Completed"; ausgeführt.

Warum? AsyncWaitHandle.WaitOne() nur warten, bis Ihr Asynchron-Vorgang abzuschließen, nicht Ihr Rückruf

Die Callback-Parameter von BeginInvoke ist eigentlich ein AsyncCallback delegate, das bedeutet, dass Ihr Rückruf asynchron aufgerufen wird. Dies ist beabsichtigt, da die Operationsergebnisse asynchron verarbeitet werden sollen (und der gesamte Zweck dieses Rückrufparameters ist).

Da die BeginInvoke-Funktion tatsächlich Ihre Callback-Funktion aufruft, dient der IAsyncResult.WaitOne-Aufruf nur der Operation und hat keinen Einfluss auf den Callback.

Siehe Microsoft documentation (Abschnitt Ausführen einer Rückrufmethode, wenn ein asynchroner Anruf beendet wird). Es gibt auch eine gute Erklärung und ein Beispiel.

Wenn der Thread, der den asynchronen Aufruf initiiert, nicht der Thread sein muss, der die Ergebnisse verarbeitet, können Sie eine Rückrufmethode ausführen, wenn der Aufruf abgeschlossen ist. Die Callback-Methode wird in einem ThreadPool-Thread ausgeführt.

Lösung

Wenn Sie sowohl für den Betrieb warten wollen und den Rückruf, müssen Sie die Signalisierung selbst behandeln. Eine ManualReset ist eine Möglichkeit, dies zu tun, die Ihnen die meiste Kontrolle gibt (und so hat Microsoft es in ihren Dokumenten gemacht).

Hier wird der Code mit ManualResetEvent geändert.

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(); 
    } 
} 

Für das Beispiel, das Sie gegeben haben, würden Sie besser einen Rückruf nicht verwenden und stattdessen das Ergebnis in Ihrer CallLongOp Funktion Handhabung, wobei in diesem Fall Ihre WaitOne auf den Betrieb delegieren fein arbeiten.

+0

danke für die Antwort. Was genau machen wir mit dem IAsyncResult, das wir von BeginInvoke() erhalten? –

+0

Sie können damit die Ausführung in der Methode stoppen, die begininvoke aufgerufen hat. I.e. Jedes Szenario, in dem Sie auf den Abschluss der Operation warten möchten. – badbod99

+0

RENNZUSTAND! Sie sollten das Ereignis vor dem Aufruf von BeginInvoke erstellen, aber das Hinzufügen weiterer Synchronisationsobjekte ist unnötig und ineffizient. –

5

Wie andere gesagt haben, result.WaitOne bedeutet nur, dass das Ziel von BeginInvoke beendet ist, und nicht der Rückruf. Setzen Sie einfach den Post-Processing-Code in den BeginInvoke Delegaten.

+0

Ok ... sehr schöne Lösung in der Tat! Aber für eine Erklärung, warum ich denke, dass ich das ziemlich gut behandelt habe. – badbod99

+0

Es ist schlau, aber wann wäre das wirklich nützlich? ManualReset gibt Ihnen die Möglichkeit zu warten, wann immer Sie warten wollen, dies ruft die Operation dann einen Callback auf, um das Ergebnis zu behandeln und wartet auf beides. Sie könnten das Ergebnis in der Operation selbst behandeln, wenn Sie das wollten. – badbod99

+0

@ badbod99: Dies ermöglicht es Ihnen, das Ergebnis zu behandeln, auch wenn Sie die Funktion nicht in 'longOpDelegate' geschrieben haben (oder es ist eine Methode einer anderen Klasse und hat keinen Zugriff auf das private 'longOpResult'-Member oder Sie will keine umgekehrte Kopplung einführen, oder ...). –

0

Ich hatte das gleiche Problem vor kurzem, und ich dachte mir eine andere Möglichkeit, es zu lösen, es funktionierte in meinem Fall. Wenn das Zeitlimit Sie nicht unterbricht, überprüfen Sie das Flag IsCompleted erneut, wenn Wait Handle Timeout ist. In meinem Fall wird das Wait-Handle signalisiert, bevor der Thread blockiert wird, und direkt nach der if-Bedingung, also überprüfe es erneut, nachdem Timeout den Trick gemacht hat.

while (!AsyncResult.IsCompleted) 
{ 
    if (AsyncWaitHandle.WaitOne(10000)) 
     break; 
} 
Verwandte Themen