2010-11-04 138 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; 
}