Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

IAsyncResult.AsyncWaitHandle.WaitOne() completes ahead of callback

Here is the code:

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

Here is the test case:

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

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

If this is run the test case can fail. Why exactly?

There is very little documentation on how delegate.BeginInvoke works. Does anyone have any insights they would like to share?

Update This is a subtle race-condition that is not well documented in MSDN or elsewhere. The problem, as explained in the accepted answer, is that when the operation completes the Wait Handle is signalled, and then the Callback is executed. The signal releases the waiting main thread and now the Callback execution enters the "race". Jeffry Richter's suggested implementation shows what's happening behind the scenes:

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

For a solution refer to Ben Voigt's answer. That implementation does not incur the additional overhead of a second wait handle.

like image 430
Noel Abrahams Avatar asked Nov 04 '10 17:11

Noel Abrahams


2 Answers

The ASyncWaitHandle.WaitOne() is signaled when the asynchronous operation completes. At the same time CallBack() is called.

This means that the the code after WaitOne() is run in the main thread and the CallBack is run in another thread (probably the same that runs DoLongOp()). This results in a race condition where the value of longOpResult essentially is unknown at the time it is returned.

One could have expected that ASyncWaitHandle.WaitOne() would have been signaled when the CallBack was finished, but that is just not how it works ;-)

You'll need another ManualResetEvent to have the main thread wait for the CallBack to set longOpResult.

like image 200
Torben Koch Pløen Avatar answered Sep 18 '22 19:09

Torben Koch Pløen


As others have said, result.WaitOne just means that the target of BeginInvoke has finished, and not the callback. So just put the post-processing code into the BeginInvoke delegate.

    //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;
like image 28
Ben Voigt Avatar answered Sep 19 '22 19:09

Ben Voigt