Saturday, June 16, 2007

Today, while working with WebRequest (HttpWebRequest) class and its BeginGetResponse method I have discovered a strange behavior. It turns out that you cannot always relay on the IAsyncResult's AsyncWaitHandler's WaitOne method as I will try to show below.

The following is a more or less a standard way to work with an asynchronous web requests (code is simplified, all constructs such as using and try/catch and timeout handling have been omitted for simplicity reason):

class RequestState
{
    public RequestState(CompleteDelegate callback, WebRequest request)
    {
        Callback = callback;
        Request = request;
    }
    public CompleteDelegate Callback;
    public WebRequest Request;
}
private static WaitHandle MakeAsyncRequest(CompleteDelegate callback)
{
    WebRequest request = WebRequest.Create("http://vaultofthoughts.net");
    RequestState state = new RequestState(callback, request);
    IAsyncResult result = request.BeginGetResponse(GetResponseComplete, state);
    return result.AsyncWaitHandle;
}
private static void GetResponseComplete(IAsyncResult result)
{
    RequestState state = (RequestState)result.AsyncState;
    WebResponse response = state.Request.EndGetResponse(result);
    state.Callback();
}

Now we can invoke MakeAsyncRequest method from anywhere in our code passing a delegate that will be called upon request completion. We get a WaitHandle as a return value so we can synchronize threads in case multiple calls were made.

Where is the problem? Look at the following code:

private void MakeCalls()
{
    WaitHandle[] waitHandles = new WaitHandle[2];
    waitHandles[0] = MakeAsyncRequest(RequestComplete);
    waitHandles[1] = MakeAsyncRequest(RequestComplete);
    // Wait until both requests complete
    WaitHandle.WaitAll(waitHandles);   
}
private void RequestComplete()
{
    //....
}

The problem is that there is no guarantee that RequestComplete method will be called 2 times before code continues beyond the WaitHandle.WaitAll call. Why is that?

At first I thought it was a bug in how .NET threads work, but a while I have come to the understanding that it is the way it should be. The WaitHandles returned from the MakeAsyncRequest method are signaled as soon as the request completes. After request completes the GetResponseComplete method is called. At this time, WaitHandle is signaled. Only at the end of the GetResponseComplete method, the callback is called (it could even be called as the first action and it wouldn't make much difference).

So how to solve the issue? The solution is to return new instance of ManualResetEvent instead of IAsyncResult.AsyncWaitHandle. We also have to pass an instance of this object with rest of the state to GetResponseComplete method so we can signal it at the end, after the callback method is called.

A complete solution looks as follows:

class RequestState
{
    public RequestState(CompleteDelegate callback, 
WebRequest request, ManualResetEvent waitHandle) { Callback = callback; Request = request; WaitHandle = waitHandle; } public CompleteDelegate Callback; public WebRequest Request; public ManualResetEvent WaitHandle; } private static WaitHandle MakeAsyncRequest(CompleteDelegate callback) { WebRequest request = WebRequest.Create("http://vaultofthoughts.net"); ManualResetEvent waitHandle = new ManualResetEvent(false); RequestState state = new RequestState(callback, request, waitHandle); IAsyncResult result = request.BeginGetResponse(GetResponseComplete, state); return waitHandle; } private static void GetResponseComplete(IAsyncResult result) { RequestState state = (RequestState)result.AsyncState; WebResponse response = state.Request.EndGetResponse(result); state.Callback(); state.WaitHandle.Set(); }

I have marked the changed parts in the code above. Notice that we don't have to change the code that is using our method.

kick it on DotNetKicks.com