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

时间:2010-11-04 17:17:13

标签: c# .net multithreading

以下是代码:

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

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

5 个答案:

答案 0 :(得分:10)

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

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

人们可以预料,当CallBack结束时,ASyncWaitHandle.WaitOne()会发出信号,但这不是它的工作方式; - )

您需要另一个ManualResetEvent让主线程等待CallBack设置longOpResult。

答案 1 :(得分: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;

答案 2 :(得分:3)

发生了什么

由于您的操作DoLongOp已完成,控制将在CallLongOp内恢复,并且该功能在回调操作完成之前完成。 Assert.IsNotNull(result);然后在longOpResult = "Completed";之前执行。

<强>为什么呢? AsyncWaitHandle.WaitOne()只会等待您的异步操作完成,而不是等待回拨

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

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

请参阅Microsoft documentation(部分执行异步调用完成时的回调方法)。还有一个很好的解释和例子。

  

如果启动异步调用的线程不需要是处理结果的线程,则可以在调用完成时执行回调方法。回调方法在ThreadPool线程上执行。

<强>解决方案

如果要等待操作和回调,则需要自己处理信令。 ManualReset是实现它的一种方式,它确实为您提供了最大的控制权(这也是Microsoft在其文档中完成它的方式)。

以下是使用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将正常工作。

答案 3 :(得分:0)

在CallLongOp方法之后执行回调。由于您只在回调中设置变量值,因此它可以为null。 阅读:link text

答案 4 :(得分:0)

我最近遇到了同样的问题,我想出了另一种方法来解决它,它适用于我的情况。但是,如果超时不对您造成影响,请在等待句柄超时时重新检查标志IsCompleted。在我的情况下,等待句柄在阻塞线程之前发出信号,并且在if条件之后立即发出信号,因此在超时后重新检查它将会发挥作用。

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