Posted by: Dennis | May 18, 2009

How to do parallel work with PageMethods

Let take an trivial example. Here we make 4 asynchronous calls to the server to some function call DoWork.

function pageLoad() {

   PageMethods.DoWork(OnSucceeded, OnFailed);

   PageMethods.DoWork(OnSucceeded, OnFailed);

   PageMethods.DoWork(OnSucceeded, OnFailed);

   PageMethods.DoWork(OnSucceeded, OnFailed);

}

The DoWork method returns the time it started and the time it finished:

[WebMethod]

public static string DoWork()

{

  DateTime dt = DateTime.Now;

  Thread.Sleep(2000);

  return "Success @ " + dt.ToString(DateTimeFormatInfo.InvariantInfo) + " - " +

    DateTime.Now.ToString(DateTimeFormatInfo.InvariantInfo);

}

If you create a brand new project in Visual Studio and run the above, you will get something similar to:

Success @ 05/18/2009 20:04:52 – 05/18/2009 20:04:54
Success @ 05/18/2009 20:04:52 – 05/18/2009 20:04:54
Success @ 05/18/2009 20:04:52 – 05/18/2009 20:04:54
Success @ 05/18/2009 20:04:52 – 05/18/2009 20:04:54

And then 10 minutes later when you try to do the exact same thing on your production environment and test it you get:

Success @ 05/18/2009 20:18:09 – 05/18/2009 20:18:11
Success @ 05/18/2009 20:18:11 – 05/18/2009 20:18:13
Success @ 05/18/2009 20:18:13 – 05/18/2009 20:18:15
Success @ 05/18/2009 20:18:15 – 05/18/2009 20:18:17

This was probably not what you were expecting. The reason is rather obscure. If you simply have a Global.asax then ASP.NET will take an exclusive lock on your session object on every call to the server for a given Session.

This mean that in practice, your users can only make one call to the server at a time.

So how to fix?

On might think that disabling the session access to the WebMethod is enough, like this:

[WebMethod(false)] <------- HERE

public static string DoWork()

But that would be incorrect. In fact that does absolutely nothing.

Instead you have to mark your whole PAGE with no session, using the setting in the aspx page:

EnableSessionState=”False”

That will again return dates that are the same for each call. Well almost at least. If we increase the number of calls to the server to e.g. 8, what we get back is:

Success @ 05/18/2009 20:24:47 – 05/18/2009 20:24:49
Success @ 05/18/2009 20:24:47 – 05/18/2009 20:24:49
Success @ 05/18/2009 20:24:47 – 05/18/2009 20:24:49
Success @ 05/18/2009 20:24:47 – 05/18/2009 20:24:49
Success @ 05/18/2009 20:24:48 – 05/18/2009 20:24:50
Success @ 05/18/2009 20:24:48 – 05/18/2009 20:24:50
Success @ 05/18/2009 20:24:49 – 05/18/2009 20:24:51
Success @ 05/18/2009 20:24:49 – 05/18/2009 20:24:51

The reason is that now the browser is restricted by opening too many connections at the same time. So even though it will look like it is calling all 8 methods on the client side (the javascript call returns, FireBug will show nice graphs saying they are all called at the same time, etc.) then in reality it waits until a connection is available.

But what do you do when you actually need to use Session somewhere in your page or you have a nice 4+ CPU machine you actually want to utilize? Well, that gets a little tricky.

First we have to change the client side. Instead of making a single webmethod call, we need to first make a call to a “Begin” method and then a call to an “End” method to get the result.

function DoSomeWork() {

   PageMethods.BeginDoWork(OnSucceededBegin, OnFailed);

}

function OnSucceededBegin(result) {

   PageMethods.EndDoWork(result, OnSucceeded, OnFailed);

}

On the server side it gets even more tricky.

   1: [WebMethod]

   2: public static string BeginDoWork()

   3: {

   4:    string g = Guid.NewGuid().ToString();

   5:    Func<string> f = () => DoWork();

   6:    IAsyncResult call = f.BeginInvoke(null, f);

   7:    lock (OnGoingWork)

   8:       OnGoingWork[g] = call;

   9:    return g;

  10: }

  11: private static readonly Dictionary<string, IAsyncResult> OnGoingWork = new Dictionary<string, IAsyncResult>();

In line 4 we create a token. In line 5 we make a lambda function which calls our original method. In line 6 we start the execution of it on another thread, notice the 2nd parameter is the lambda function. Line 7 to 9 stores the IAsyncResult in a static variable using the token as identifier.

   1: [WebMethod]

   2: public static string EndDoWork(string guid)

   3: {

   4:    IAsyncResult call;

   5:    lock (OnGoingWork)

   6:    {

   7:       call = OnGoingWork[guid];

   8:       OnGoingWork.Remove(guid);

   9:    }

  10:    Func<string> f = (Func<string>) call.AsyncState;

  11:    call.AsyncWaitHandle.WaitOne();

  12:    return f.EndInvoke(call);

  13: }

In the “End” function we then need to get the result of the asynchronous call. Line 5 to 9 fetches the IAsyncResult out of the static variable and cleans up the. Line  10 gets the lambda function out of the IAsyncResult with the small hack from line 6 in the “Begin” function. Line 11 waits for the call to actually finish, and then in line 12 we get the result.

And voila:

Success @ 05/18/2009 20:28:48 – 05/18/2009 20:28:50
Success @ 05/18/2009 20:28:48 – 05/18/2009 20:28:50
Success @ 05/18/2009 20:28:48 – 05/18/2009 20:28:50
Success @ 05/18/2009 20:28:48 – 05/18/2009 20:28:50
Success @ 05/18/2009 20:28:48 – 05/18/2009 20:28:50
Success @ 05/18/2009 20:28:48 – 05/18/2009 20:28:50
Success @ 05/18/2009 20:28:48 – 05/18/2009 20:28:50
Success @ 05/18/2009 20:28:49 – 05/18/2009 20:28:51

About these ads

Responses

  1. “If you simply have a Global.asax then ASP.NET will take an exclusive lock on your session object on every call to the server for a given Session.”

    => ASP.NET won’t lock the session object if you remove Session_Start and Session_End handlers from Global.asax. :-)


Categories

Follow

Get every new post delivered to your Inbox.

%d bloggers like this: