2010-11-04 161 views
9

的下面是代碼:IAsyncResult.AsyncWaitHandle.WaitOne()提前完成回調

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

下面是測試情況:

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

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

如果這是運行測試用例可能會失敗。爲什麼呢?

很少有關於delegate.BeginInvoke如何工作的文檔。有沒有人有任何見解他們想分享?

更新 這是一個微妙的競爭條件,MSDN或其他地方沒有很好的記錄。正如接受的答案中所解釋的那樣,問題是當操作完成時,等待手柄會發出信號,然後執行回調。信號釋放等待的主線程,現在回調執行進入「比賽」。 Jeffry Richter's suggested implementation顯示發生了什麼幕後:

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

有關解決方案請參閱本福格特的答案。該實現不會導致第二個等待句柄的額外開銷。

+0

刪除回調並重試。 – jgauffin 2010-11-04 17:22:56

+0

@jgauffin,如果你注意到這個問題不是問「我如何得到這個工作?」顯然這是一個人爲的例子。 – 2010-11-04 17:39:39

+0

您的問題是:「如果運行該測試用例可能會失敗,爲什麼?」。我*做了*回答。因爲您嘗試混合處理異步操作的兩種非常不同的方式。 – jgauffin 2010-11-04 17:57:29

回答

8

當異步操作完成時,ASyncWaitHandle.WaitOne()會發出信號。同時調用CallBack()。

這意味着WaitOne()之後的代碼在主線程中運行,並且CallBack在另一個線程中運行(可能與運行DoLongOp()的運行方式相同)。這會導致競爭條件,longOpResult的值在返回時基本上是未知的。

人能預料ASyncWaitHandle.WaitOne()當回調結束就已經發出信號,但這是不如何工作;-)

你需要另一個ManualResetEvent的有主線程等待CallBack設置longOpResult。

0

該回調在CallLongOp方法之後執行。由於您只在回調中設置了變量值,因此可以認爲它是空的。 閱讀:link text

+0

也就是說,您正在查找的結果尚未設置爲直到CallLongOp方法返回後才調用回調函數si。 – Kell 2010-11-04 17:44:10

+0

感謝您的迴應。在CallLongOp方法之後,並不總是執行回調。嘗試把Thread.Sleep(500);在返回longOpResult之前在CallLongOp中;並且測試會通過。 – 2010-11-04 17:47:38

3

發生了什麼

由於您的操作DoLongOp已完成,控制簡歷中CallLongOp和回調操作完成之前的函數完成。然後在longOpResult = "Completed";之前執行Assert.IsNotNull(result);

爲什麼? AsyncWaitHandle.WaitOne()僅會等待你的異步操作來完成,而不是你的回調

的BeginInvoke的回調參數實際上是一個AsyncCallback delegate是,這意味着你的回調異步調用。這是通過設計,目的是異步處理操作結果(並且是此回調參數的整體目的)。

由於BeginInvoke函數實際調用您的回調函數,IAsyncResult.WaitOne調用僅用於操作,不影響回調。

查看Microsoft documentation(部分當異步呼叫完成時執行回撥方法)。還有一個很好的解釋和例子。

如果啓動異步調用的線程不需要是處理結果的線程,則可以在調用完成時執行回調方法。回調方法在ThreadPool線程上執行。

解決方案

如果你想等待操作和回調都,你需要處理信令自己。 A ManualReset是這樣做的一種方式,它肯定會給你最大的控制權(而這正是微軟在他們的文檔中所做的)。

這裏是使用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(); 
    } 
} 

對於你給的例子中,你會過得更好沒有使用回調,而是處理結果在CallLongOp功能,在這種情況下,您的操作代表了WaitOne將正常工作。

+0

感謝您的回覆。那麼我們對從BeginInvoke()得到的IAsyncResult做了什麼? – 2010-11-04 18:27:42

+0

您可以使用它來暫停調用begininvoke的方法中的執行。即任何您想等待操作本身完成的場景。 – badbod99 2010-11-04 18:39:02

+0

比賽條件!你最好在調用'BeginInvoke'之前創建事件,但添加更多的同步對象是不必要的,也是效率低下的。 – 2010-11-04 21:23:56

5

正如其他人所說,result.WaitOne只是意味着BeginInvoke的目標已經完成,而不是回調。因此,只需將後處理代碼放入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

好的...非常好的解決方案!但是爲什麼我認爲我已經覆蓋了很好的解釋。 – badbod99 2010-11-04 21:34:49

+0

這很聰明,但什麼時候這實際上會有用? ManualReset使您可以控制等待的任何時候,這會調用操作,然後調用回調來處理結果並一次等待。如果這是你想要的,你可以在操作本身中處理結果。 – badbod99 2010-11-04 21:56:33

+0

@ badbod99:這允許你處理結果,即使你沒有寫入填充到'longOpDelegate'中的函數(或者它是另一個類的方法,並且不能訪問私有的'longOpResult'成員,或者你不想引入反向耦合,或...)。 – 2010-11-05 01:21:37

0

最近我有同樣的問題,我想出另一種方式來解決這個問題,它的工作在我的情況。如果超時沒有影響到你,重要的是當Wait Handle超時時重新檢查IsCompleted標誌。在我的情況下,等待句柄在阻塞線程之前發出信號,並在if條件之後立即發出,所以在超時之後重新檢查它是否有用。

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