代码之家  ›  专栏  ›  技术社区  ›  Zephyr was a Friend of Mine

IAsyncResult.AsyncWaitHandle.WaitOne()在回调之前完成

  •  9
  • Zephyr was a Friend of Mine  · 技术社区  · 15 年前

    代码如下:

    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或其他地方。正如接受的答案中所解释的,问题是当操作完成时,等待句柄会发出信号,然后执行回调。信号释放等待的主线程,现在回调执行进入“race”。 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);
    

    有关解决方案,请参阅Ben Voigt的答案。该实现不会产生第二个等待句柄的额外开销。

    5 回复  |  直到 15 年前
        1
  •  10
  •   Torben Koch Pløen    15 年前

    异步操作完成时,会发出ASyncWaitHandle.WaitOne()信号。同时调用CallBack()。

    这意味着WaitOne()之后的代码在主线程中运行,回调在另一个线程中运行(可能与运行DoLongOp()的代码相同)。这将导致一个竞争条件,其中longOpResult的值在返回时本质上是未知的。

    您需要另一个ManualResetEvent,使主线程等待回调设置longOpResult。

        2
  •  5
  •   Ben Voigt    15 年前

    正如其他人所说, result.WaitOne 就是说 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;
    
        3
  •  3
  •   badbod99    15 年前

    发生了什么事

    自从你手术后 DoLongOp 已完成,控件将在 CallLongOp 函数在回调操作完成之前完成。 Assert.IsNotNull(result); 然后在之前执行 longOpResult = "Completed"; .

    AsyncWaitHandle.WaitOne() 只等待异步操作完成,而不是回调

    BeginInvoke的回调参数实际上是 AsyncCallback delegate ,这意味着您的回调是异步调用的。这是设计的,因为目的是异步处理操作结果(这是回调参数的全部目的)。

    由于BeginInvoke函数实际上调用回调函数,因此IAsyncResult.WaitOne调用仅用于操作,不影响回调。

    Microsoft documentation (剖面图 异步调用完成时执行回调方法 ). 还有一个很好的解释和例子。

    解决方案

    如果要同时等待操作和回调,则需要自己处理信令。一个 ManualReset 是一种能给你最大控制权的方法(这就是微软在他们的文档中是如何做到的)。

    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可以正常工作。

        4
  •  0
  •   Kell    15 年前

    回调在CallLongOp方法之后执行。因为您只在回调中设置变量值,所以它应该是空的。 link text

        5
  •  0
  •   zhang lin    9 年前

    我最近也遇到了同样的问题,我想出了另一个办法来解决它,在我的情况下是有效的。如果超时不在您身边,请在等待句柄为timeout时重新检查标志IsCompleted。在我的例子中,等待句柄在阻塞线程之前和if条件之后发出信号,所以在超时之后重新检查它就可以了。

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