Amazon.com Widgets Scalability

WilliaBlog.Net

I dream in code

About the author

Robert Williams is an internet application developer for the Salem Web Network.
E-mail me Send mail
Code Project Associate Logo
Go Daddy Deal of the Week: 30% off your order at GoDaddy.com! Offer expires 11/6/12

Recent comments

Archive

Authors

Tags

Disclaimer

The opinions expressed herein are my own personal opinions and do not represent my employer's view in anyway.


Custom Threading in ASP.NET

or "One Mans obsession with finding a way to call synchronous methods asynchronously in ASP.NET".

This is for anyone interested in exploring the System.Threading Namespace in ASP.NET. There are many wrong ways to do it, one right way, and one other way that should only be used when you have no alternative.

First, the wrong ways. As you may or may not know, in .NET you can call any synchronous method asynchronously simply by creating a delegate method and calling the delegate’s BeginInvoke and EndInvoke methods. Knowing this, you might be tempted to try this in ASP.NET. For example, suppose you are using a prebuilt library object that contains the following method which makes a synchronous WebRequest:

        private static readonly Uri c_UrlImage1 = new Uri(@"http://williablog.net/williablog/image.axd?picture=2010%2f1%2fSlide6.JPG");

        private HttpWebRequest request;

        public string Result; // Public Variable to store the result where applicable

 

        public string SyncMethod()

        {

            // prepare the web page we will be asking for

            request = (HttpWebRequest)WebRequest.Create(c_UrlImage1);

 

            // execute the request

            HttpWebResponse response = (HttpWebResponse)request.GetResponse();

            return String.Format("Image at {0} is {1:N0} bytes", response.ResponseUri, response.ContentLength);

        }

Then you read my article on MultiThreading in asp.net and decide that this should be an Asynchronous call. If you don't have easy access to the source code of that prebuilt library, you might be tempted to try this:

 

        public delegate string AsyncMthodDelegate();

 

        public IAsyncResult BeginUpdateByDelegate(object sender, EventArgs e, AsyncCallback cb, object state)

        {

            AsyncMthodDelegate o_MyDelegate = SyncMethod;

            return o_MyDelegate.BeginInvoke(cb, state);

        }

 

        public void EndUpdateByDelegate(IAsyncResult ar)

        {

            AsyncMthodDelegate o_MyDelegate = (AsyncMthodDelegate)((AsyncResult)ar).AsyncDelegate;

            Result = o_MyDelegate.EndInvoke(ar);

            lblResult.Text = Result;

        }

 

        protected void Page_Load(object sender, EventArgs e)

        {

            RegisterAsyncTask(new PageAsyncTask(BeginUpdateByDelegate, EndUpdateByDelegate, AsyncUpdateTimeout, null, false));

        }

 

        public void AsyncUpdateTimeout(IAsyncResult ar)

        {

            Label1.Text = "Connection Timeout";

        }

 

On paper this looks great. You just converted a synchronous method to an asynchronous method and called it using RegisterAsyncTask. In spite of how promising this technique looks, I'm sorry to say that it will do nothing for scalability or performance. Unfortunately, the thread used by BeginInvoke is actually taken from the same worker thread pool that is used by ASP.Net to handle Page Requests, so what you are actually doing is returning the main thread to the thread pool when BeginUpdateByDelegate returns, grabbing a second thread from the same thread pool to call BeginInvoke, blocking that thread until EndInvoke is called, then returning thread two to the thread pool. ASP.NET then pulls a third thread from the same pool to call EndUpdateByDelegate, and complete the request. Net gain: 0 threads, and your code is harder to read, debug and maintain.

 

OK. What about using ThreadPool.QueueUserWorkItem()? You rewrite your code again and now it looks like this:

 

 

        public IAsyncResult BeginThreadPoolUpDate(object sender, EventArgs e, AsyncCallback cb, object state)

        {

            AsyncHelper helper = new AsyncHelper(cb, state);

            ThreadPool.QueueUserWorkItem(ThreadProc, helper);

            return helper;

        }

 

        public void EndThreadPoolUpDate(IAsyncResult AR)

        {

            AsyncHelper helper = (AsyncHelper)AR;

 

            // If an exception occurred on the other thread, rethrow it here

            if (helper.Error != null)

            {

                throw helper.Error;

            }

 

            // Otherwise retrieve the results

            Result = (string)helper.Result;

            lblResult.Text = Result;

        }

 

        protected void Page_Load(object sender, EventArgs e)

        {

            RegisterAsyncTask(new PageAsyncTask(BeginThreadPoolUpDate, EndThreadPoolUpDate, AsyncUpdateTimeout, null, false));

        }

 

        public void AsyncUpdateTimeout(IAsyncResult ar)

        {

            Label1.Text = "Connection Timeout";

        }

 

        class AsyncHelper : IAsyncResult

        {

            private AsyncCallback _cb;

            private object _state;

            private ManualResetEvent _event;

            private bool _completed = false;

            private object _lock = new object();

            private object _result;

            private Exception _error;

 

            public AsyncHelper(AsyncCallback cb, object state)

            {

                _cb = cb;

                _state = state;

            }

 

            public Object AsyncState

            {

                get { return _state; }

            }

 

            public bool CompletedSynchronously

            {

                get { return false; }

            }

 

            public bool IsCompleted

            {

                get { return _completed; }

            }

 

            public WaitHandle AsyncWaitHandle

            {

                get

                {

                    lock (_lock)

                    {

                        if (_event == null)

                            _event = new ManualResetEvent(IsCompleted);

                        return _event;

                    }

                }

            }

 

            public void CompleteCall()

            {

                lock (_lock)

                {

                    _completed = true;

                    if (_event != null)

                        _event.Set();

                }

 

                if (_cb != null)

                    _cb(this);

            }

 

            public object Result

            {

                get { return _result; }

                set { _result = value; }

            }

 

            public Exception Error

            {

                get { return _error; }

                set { _error = value; }

            }

        }

    }

Yikes, this time a helper class was needed to provide the IAsyncResult Interface. Now your code is even more unreadable and I'm sorry to tell you that the thread used by ThreadPool.QueueUserWorkItem also comes from the same thread pool that is used by ASP.Net for handling Requests.

Fine. I'll just use Thread.Start() and create my own thread. Well, you could do that - by creating your own thread, you cannot be stealing one from the ASP.Net threadpool. But not so fast! If you have determined that this method needs to be made asynchronous for reasons of scalability, then this page is under heavy load. Think about that for a second. If you are creating a new thread every time your page is requested and your page is being hammered with 1000 requests a second, then you are creating 1000 new threads almost simultaneously and they are all fighting for CPU time. Clearly using Thread.Start() risks unconstrained thread growth and you can easily find yourself creating so many new threads that the increased CPU contention actually decreases rather than increases scalability, so I don't recommend using Thread.Start() in ASP.Net.

So, you troll the internet looking for other ways.  In the bowels of the System.Threading Namespace, you find the ThreadPool.UnsafeQueueNativeOverlapped method that promises to open up an I/O Completion Port Thread, drawn from the I/O thread pool, just for you to run your method on. So you modify your code again, recompiling with the Allow unsafe code box checked. Now it looks something like this:

 

        public IAsyncResult BeginIOCPUpDate(object sender, EventArgs e, AsyncCallback cb, object state)

        {

            AsyncHelper helper = new AsyncHelper(cb, state);

            IOCP.delThreadProc myDel = SyncMethod;

            IOCP myIOCp = new IOCP(myDel);

 

            try

            {

                myIOCp.RunAsync();

            }

            catch (Exception ex)

            {

                helper.Error = ex;

            }

            finally

            {

                helper.CompleteCall();

            }

            return helper;

        }

 

        public void EndIOCPUpDate(IAsyncResult AR)

        {

            AsyncHelper helper = (AsyncHelper)AR;

 

            // If an exception occurred on the other thread, rethrow it here

            if (helper.Error != null)

            {

                throw helper.Error;

            }

 

            // Otherwise retrieve the results

            Result = (string)helper.Result;

            lblResult.Text = Result;

        }

 

        protected void Page_Load(object sender, EventArgs e)

        {

            RegisterAsyncTask(new PageAsyncTask(BeginIOCPUpDate, EndIOCPUpDate, AsyncUpdateTimeout, null, false));

        }

 

        public void AsyncUpdateTimeout(IAsyncResult ar)

        {

            Label1.Text = "Connection Timeout";

        }

 

        class IOCP

        {

 

            public delegate string delThreadProc();

            private readonly delThreadProc _delThreadProc;

 

            public IOCP(delThreadProc ThreadProc)

            {

                _delThreadProc = ThreadProc;

            }

 

            public void RunAsync()

            {

                unsafe

                {

                    Overlapped overlapped = new Overlapped(0, 0, IntPtr.Zero, null);

                    NativeOverlapped* pOverlapped = overlapped.Pack(IocpThreadProc, null);

                    ThreadPool.UnsafeQueueNativeOverlapped(pOverlapped);

                }

            }

 

            unsafe void IocpThreadProc(uint x, uint y, NativeOverlapped* p)

            {

                try

                {                 

                    _delThreadProc();

                }

                finally

                {

                    Overlapped.Free(p);

                }

            }

 

        }

 

        class AsyncHelper : IAsyncResult

        {

            // Code ommitted for clarity: see above for the full AsyncHelper Class          


        }

 

It seems to work but how can you be sure? You add the following code at various points to test:

            Label1.Text += "<b>EndIOCPUpDate</b><br />";

            Label1.Text += "CompletedSynchronously: " + AR.CompletedSynchronously + "<br /><br />";

            Label1.Text += "isThreadPoolThread: " + System.Threading.Thread.CurrentThread.IsThreadPoolThread.ToString() + "<br />";

            Label1.Text += "ManagedThreadId : " + System.Threading.Thread.CurrentThread.ManagedThreadId + "<br />";

            Label1.Text += "GetCurrentThreadId : " + AppDomain.GetCurrentThreadId() + "<br />";

            Label1.Text += "Thread.CurrentContext : " + System.Threading.Thread.CurrentContext.ToString() + "<br />";

 

            int availWorker = 0;

            int maxWorker = 0;

            int availCPT = 0;

            int maxCPT = 0;

            ThreadPool.GetAvailableThreads(out availWorker, out availCPT);

            ThreadPool.GetMaxThreads(out maxWorker, out maxCPT);

            Label1.Text += "--Available Worker Threads: " + availWorker.ToString() + "<br />";

            Label1.Text += "--Maximum Worker Threads: " + maxWorker.ToString() + "<br />";

            Label1.Text += "--Available Completion Port Threads: " + availCPT.ToString() + "<br />";

            Label1.Text += "--Maximum Completion Port Threads: " + maxCPT + "<br />";

            Label1.Text += "===========================<br /><br />";

 

And you discover that while it does indeed execute on an I/O thread it also uses or blocks a worker thread, perhaps to run the delegate method. In any case, net gain -1 threads - at least with the two previous techniques you were only using a worker thread. Now you are using a worker thread and an I/O thread, and using unsafe code, and your code looks even worse!

So, you think for a while and decide to use a custom threadpool. Perhaps you recall that the folks at Wintellect.com used to offer a free custom threadpool in their PowerThreading Library. You find a copy among your archives and set it up. (The code looks just like the Threadpool.QueueUserWorkItem code above, but it is using the custom thread pool. You add your Code to verify and yes, it does everything you need it to. Available worker Threads = Maximum Worker Threads, Available I/O Threads = Maximum I/O threads. No threads are being stolen from ASP.Net and you don't risk unconstrained thread growth. Short of finding a way to add the asynchronous method to the prebuilt dll (obtaining and modifying the source code - contacting the author/vendor to request asynchronous calls be addedto their library) or switching to your own code, or to another library that already supports asynchronous methods, this is your only option.

The only right way to do asynchronous programming in asp.net is to find a way to add true asynchronous capabilities to your library. If this is impossible, then a custom threadpool is your only option. Now before you rush off to wintellect.com to download this magical solution, I should warn you that the custom threadpool is no longer part of the power threading library. Curious to understand why I contacted the author (Mr Jeffrey Richter) and he told me that he removed the custom thread pool as he believed it promoted poor prgramming practice. He explained that the default number of threads has been greatly increased with the introduction of the 3.x framework (and of course you can increase it in the machine.config), but that ultimately if your library does not support Asynchronous I/O calls, your application will not be scalable.

“If they don’t support async operations then they are not usable for scalable apps – period... Personally, I would not use a [library] that didn't support async operations unless I was using it in a situation where I knew I would always have just a few clients.”

I encourage you to use Asynchronous Delegates and ThreadPool.QueueUserWorkItem freely in console applications and windows forms programs,  just don't bother using them in ASP.Net as it is a waste of time.

So. There you have it. The sample code I used to test which thread pool threads were used in each of the techniques discussed above can be downloaded here. The sample includes a version of Wintellect's Power Threading Library that still contains the Custom Threadpool - look for it in the bin folder.

AsyncThreadTests.zip (264.52 kb)

Note: I recommend you test under IIS 6.0 or IIS 7.0: running on the Casini web server or on IIS 5.1 under windows XP shows inconsistant results.


Posted by Williarob on Tuesday, December 16, 2008 10:19 AM
Permalink | Comments (0) | Post RSSRSS comment feed

Instantly Increase ASP.NET Scalability

Each time a request for a .NET resource (.aspx, page, .ascx control, etc.) comes in a thread is grabbed from the available worker thread pool in the asp.net worker process (aspnet_wp.exe on IIS 5.x, w3wp.exe on IIS 6/7) and is assigned to a request. That thread is not released back into the thread pool until the final page has been rendered to the client and the request is complete.

Inside the ASP.Net Worker Process there are two thread pools. The worker thread pool handles all incoming requests and the I/O Thread pool handles the I/O (accessing the file system, web services and databases, etc.). But how many threads are there in these thread pools? I had assumed that the number of threads would vary from machine to machine – that ASP.NET and IIS would carefully balance the number of available threads against available hardware, but that is simply not the case. ASP.Net installs with a fixed, default number of threads to play with: The CLR for the 1.x Framework sets these defaults at just 20 worker threads and 20 I/O thread per CPU. Now this can be increased by modifying the machine.config, but if you are not aware of this, then 20 threads is all you’re playing with. If you have multiple sites sharing the same worker process, then they are all sharing this same thread pool.

So long as the number of concurrent requests does not exceed the number of threads available in the pool, all is well. But when you are building enterprise level applications the thread pool can become depleted under heavy load, and remember by default heavy load is more than just 20 simultaneous requests. When this happens, new requests are entered into the request queue (and the users making the requests watch an hour glass spin). ASP.NET will allow the request queue to grow only so big before it starts to reject requests at which point it starts returning Error 503, Service Unavailable.

If you are not aware of this “Glass Ceiling of Scalability”, this is a perplexing error – one that never happened in testing and may not reproducible in your test environment, as it only happens under extreme load.

So the first thing you can do to improve scalability is to raise the values. The defaults for the ASP.NET 2.0 are 100 threads in each pool per CPU and the defaults for the ASP.NET 3.x CLR is 250 per CPU for worker threads and 1000 per CPU for I/O threads, however you can tune it further using the guidelines below. 32 bit windows can handle about 1400 concurrent threads, 64 bit windows can handle more, though I don’t have the figures.

You can tune ASP.NET thread pool using maxWorkerThreads, minWorkerThreads, maxIoThreads, minFreeThreads, minLocalRequestFreeThreads and maxconnection attributes in your Machine.config file. Here are the default settings.

<system.net>
  <connectionManagement>
     <add address="*" maxconnection="2" />
  </connectionManagement>
</system.net>
<system.web>
  <httpRuntime minFreeThreads="8" minLocalRequestFreeThreads="4"  />
  <processModel maxWorkerThreads="100" maxIoThreads="100"  />
</system.web>

Here is the formula to reduce contention. Apply the recommended changes that are described below, across the settings and not in isolation.

  • Configuration setting Default value (.NET Framework 1.1) Recommended value
  • maxconnection 2 12 * #CPUs
  • maxIoThreads 20 100
  • maxWorkerThreads 20 100
  • minFreeThreads 8 88 * #CPUs
  • minLocalRequestFreeThreads 4 76 * #CPUs
  • Set maxconnection to 12 * # of CPUs. This setting controls the maximum number of outgoing HTTP connections that you can initiate from a client. In this case, ASP.NET is the client. Set maxconnection to 12 * # of CPUs.
  • Set maxIoThreads to 100. This setting controls the maximum number of I/O threads in the .NET thread pool. This number is automatically multiplied by the number of available CPUs. Set maxloThreads to 100.
  • Set maxWorkerThreads to 100. This setting controls the maximum number of worker threads in the thread pool. This number is then automatically multiplied by the number of available CPUs. * Set maxWorkerThreads to 100.
  • Set minFreeThreads to 88 * # of CPUs. This setting is used by the worker process to queue all the incoming requests if the number of available threads in the thread pool falls below the value for this setting. This setting effectively limits the number of requests that can run concurrently to maxWorkerThreads – minFreeThreads. Set minFreeThreads to 88 * # of CPUs. This limits the number of concurrent requests to 12 (assuming maxWorkerThreads is 100).
  • Set minLocalRequestFreeThreads to 76 * # of CPUs. This setting is used by the worker process to queue requests from localhost (where a Web application sends requests to a local Web service) if the number of available threads in the thread pool falls below this number. This setting is similar to minFreeThreads but it only applies to localhost requests from the local computer. Set minLocalRequestFreeThreads to 76 * # of CPUs.

Note The recommendations that are provided in this section are not rules. They are a starting point. Test to determine the appropriate settings for your scenario. If you move your application to a new computer, ensure that you recalculate and reconfigure the settings based on the number of CPUs in the new computer.

By raising these values, you raise the “glass ceiling of scalability”, and in many cases that may be all you need to do, but what happens when you start getting more than 250 simultaneous requests? To make optimum use of the thread pool all IO requests that you know could take a second or more to process should be made asynchronously. More information on Asynchronous programming in ASP.NET is coming soon.

I recommend testing your new machine.config locally or virtually somewhere first because if you make a mistake - for example you paste this in to the wrong area, or there is already a <system.web> element and you paste in a second one - ALL websites on the box will stop functioning as ASP.Net will not be able to parse the machine.config!!!


Posted by Williarob on Tuesday, December 02, 2008 7:41 AM
Permalink | Comments (1) | Post RSSRSS comment feed

Add Asynchronous Data Methods to the Enterprise Library

A Data Access Layer that does not support Asychronous I/O is not scalable. Period. However, since Microsoft kindly provided the source code along with the Enterprise Library, it is possible to make the Enterprise Library's Data Application Block scalable by adding the Asynchronous methods (BeginExecuteNonQuery, BeginExecuteReader, etc.) to the SQL Database Object. I took the code from the 3.x library and modified the file SqlDatabase.cs found here c:\EntLib3Src\App Blocks\Src\Data\Sql (assuming you installed the source files to the root of your C drive). The following download includes the modified binaries and my SqlDatabase.cs code file:

AsyncEL.zip (348.39 kb)

I don't think I implemented every overload, and again this is for the 3.0 library, but you should be able to modify the 4.x libraries and add more overloads by following the patterns I'm using. To use the binaries included in the zip file above, simply drop them into the bin folder of your web site or web application project, add references if necessary and don't forget to add the async=true attribute to both your <% page %>  tag and your connection string! Here is a simple example of how you might use it, to get you started:

 

    1 using System;

    2 using System.Collections;

    3 using System.Configuration;

    4 using System.Data;

    5 using System.Data.SqlClient;

    6 using System.Linq;

    7 using System.Web;

    8 using System.Web.Configuration;

    9 using System.Web.Security;

   10 using System.Web.UI;

   11 using System.Web.UI.HtmlControls;

   12 using System.Web.UI.WebControls;

   13 using System.Web.UI.WebControls.WebParts;

   14 using System.Xml.Linq;

   15 

   16 public partial class temp : System.Web.UI.Page

   17 {

   18     protected void Page_Load(object sender, EventArgs e)

   19     {

   20         if (!IsPostBack)

   21         {

   22             RegisterAsyncTask(new PageAsyncTask(new BeginEventHandler(BeginUpdateByAsyncEL), new EndEventHandler(EndUpdateByAsyncEL), new EndEventHandler(AsyncUpdateTimeout), null, true));

   23         }

   24     }

   25 

   26     /// <summary>

   27     /// Creates a SqlDatabase Object and stores it in HttpContext for the duration of the request.

   28     /// </summary>

   29     /// <value></value>

   30     /// <returns>A SqlDatabase Object</returns>

   31     /// <remarks></remarks>

   32     public static Microsoft.Practices.EnterpriseLibrary.Data.Sql.SqlDatabase AsyncNorthWindDB

   33     {

   34         get

   35         {

   36             if (HttpContext.Current == null)

   37             {

   38                 return new Microsoft.Practices.EnterpriseLibrary.Data.Sql.SqlDatabase(WebConfigurationManager.ConnectionStrings["AsyncNorthwind"].ConnectionString);

   39             }

   40             if (HttpContext.Current.Items["AsyncNorthWindDB"] == null)

   41             {

   42                 HttpContext.Current.Items.Add("AsyncNorthWindDB", new Microsoft.Practices.EnterpriseLibrary.Data.Sql.SqlDatabase(WebConfigurationManager.ConnectionStrings["AsyncNorthwind"].ConnectionString));

   43             }

   44             return HttpContext.Current.Items["AsyncNorthWindDB"];

   45         }

   46     }

   47 

   48     /// <summary>

   49     /// Begins an asynchronous call the the Northwind Database 

   50     /// </summary>

   51     /// <param name="sender"></param>

   52     /// <param name="e"></param>

   53     /// <param name="cb"></param>

   54     /// <param name="state"></param>

   55     /// <returns>An IAsyncResult Interface</returns>

   56     /// <remarks>These methods do not exist in the standard Enterprise Library</remarks>

   57     public IAsyncResult BeginUpdateByAsyncEL(object sender, EventArgs e, AsyncCallback cb, object state)

   58     {

   59         SqlCommand cmd = new SqlCommand("Update Products SET UnitPrice = 79.0 WHERE productID = 20");

   60         return AsyncNorthWindDB.BeginExecuteNonQuery(cmd, cb, state);

   61     }

   62 

   63     public void EndUpdateByAsyncEL(IAsyncResult ar)

   64     {

   65         AsyncNorthWindDB.EndExecuteNonQuery(ar);

   66     }

   67 

   68     /// <summary>

   69     /// Async Timeout method

   70     /// </summary>

   71     /// <param name="ar"></param>

   72     /// <remarks></remarks>

   73     public void AsyncUpdateTimeout(IAsyncResult ar)

   74     {

   75         Label1.Text = "Connection Timeout";

   76     }

   77 }

 

Download the Full Enterprise Library (including the complete source code) from http://www.codeplex.com/entlib.

 

kick it on DotNetKicks.com


Posted by Williarob on Monday, December 01, 2008 8:50 AM
Permalink | Comments (0) | Post RSSRSS comment feed

Begin/End Async WebService Proxy Methods No Longer Generated

If you have added a Web Reference for a Web Service recently you may have noticed that the wsdl tool no longer creates Begin and End methods for you, instead it implements what is known as the Event based Async Pattern. Which is fine if you are building a Windows or Console application, but if you are trying to call a web service asynchronously using the AddOnPreRenderAsync or RegisterAsyncTask methods in ASP.NET you really need a Begin method that returns an IAsyncResult Interface. There are several ways you can work around this.

You could, assuming you still have it, create a project in Visual Studio 2003, add the web reference there and copy it to your 2005 project, or you can run the wsdl tool manually and use the /parameters option to specify a configuration file with the "oldAsync" flag. For example, for a reference to http://my.server.com/service, which creates a proxy class with both begin/end (a.k.a. "old style") and event-based (a.k.a. "new style") methods, you'd use the command

  wsdl.exe /parameters:MyParameters.xml http://my.server.com/service

Where MyParameters.xml is as below:

MyParameters.xml:

<wsdlParameters xmlns="http://microsoft.com/webReference/">
  <nologo>true</nologo>
  <parsableerrors>true</parsableerrors>
  <sharetypes>true</sharetypes>
  <webReferenceOptions>
    <verbose>false</verbose>
    <codeGenerationOptions>properties oldAsync newAsync</codeGenerationOptions>
    <style>client</style>
  </webReferenceOptions>
</wsdlParameters>

However, my preferred method is to set a project level property WebReference_EnableLegacyEventingModel to true.

To do that, you need unload the project from Visual Studio, and edit the project file directly in a text editor. (I recommend creating a backup first)

In the first section of PropertyGroup, you will see many properties like:

    <ProjectGuid>{F4DC6946-F07E-4812-818A-F35C5E34E2FA}</ProjectGuid>
    <OutputType>Exe</OutputType>
...

Don't change any of those, but do add a new property into that section:


    <WebReference_EnableLegacyEventingModel>true</WebReference_EnableLegacyEventingModel>


Save the file, and reload the project into Visual Studio.

After that, you have to regenate all proxy code (by updating the reference, or running the custom tool on the .map file)

This is my preferred method because unlike the others I described, this one lets you refresh or update the web reference as normal, which if your project is shared with other developers is a big plus!


Posted by Williarob on Monday, December 01, 2008 7:12 AM
Permalink | Comments (2) | Post RSSRSS comment feed

Asynchronous DataSet

If you haven't yet read the article "Scalable Apps with Asynchronous Programming in ASP.NET" by Jeff Prosise or seen his presentation at TechEd then you should cetainly take the time to do so, however I'll summarize the key points briefly here. Basically, there is a finite number of Threads available to ASP.Net for request handling, and by making database calls the way that most textbooks and articles recommend, many of the available threads that should be handling requests are actually tapping their feet waiting for your database request to complete, before it can serve your page and return to the thread pool. When all the threads are busy, incoming requests are queued, and if that queue becomes too long then users start to see HTTP 503 Service unavailable errors. In other words there is a glass ceiling to scalability, with synchronous IO requests in asp.net.

 To make optimum use of the thread pool all IO requests that you know could take a second or more to process should be made asynchronously and the links above will give you plenty of examples of how you should do this. The purpose of this article is to demonstrate how you can return a DataSet asynchronously, which is not something I could find an example of anywhere. Another thing I could not find in my research was how to use asynchronous database calls when you have a datalayer, as opposed to a page or usercontrol that contacts the database directly, and I will provide you with both here.

If you have looked at examples of asynchronous database calls elsewhere on the web before arriving here you have probably become familair with the BeginExecuteReader and EndExecuteReader methods of the SQLClient namespace, but where is the BeginExecuteDataSet method? If you need a dataset, why should that have to be a synchronous request? I could not find any asynchronous methods for the DataAdapter and while I did find ways to call a WebMethod or WebService that returns a dataset asynchronously, why should you have to break your methods that return datasets out to webservices? I also found some examples of how to create delegates or use System.Threading.Thread.Start to create datasets in their own thread, but, according to Mr. Prosise these are worthless in ASP.Net because both of these methods actually steal threads for the same thread pool ASP.Net is using anyway! So by using System.Threading.Thread.Start to create your dataset all you are doing is returning a thread to the threadpool and immediately grabbing another one. So how can you do it?

For this example I borrowed the Datalayer from the Job Site Starter Kit and added some Asynchronous methods to it. Here is my BeginExecuteDataSet method:

       Public Function BeginExecuteDataSet(ByVal callback As System.AsyncCallback, _
          ByVal stateObject As Object, ByVal behavior As CommandBehavior) As System.IAsyncResult   
             Dim res As IAsyncResult = Nothing
             Me.Open()
             res = cmd.BeginExecuteReader(callback, stateObject, behavior)
             Return res
       End Function

But how is that different to BeginExecuteReader? It is not it is exactly the same, I don't see the need to rewrite the DataAdapter class from scratch to support Asynchronous functions when I can simply use a datareader to populate a dataset. The key differences are in the EndExecuteDataSet Method:

       Public Function EndExecuteDataSet(ByVal asyncResult As System.IAsyncResult) As DataSet   
             Dim ds As Dataset = Nothing
             Dim rdr As SqlClient.SqlDataReader = cmd.EndExecuteReader(asyncResult)
             Dim dt As DataTable = New DataTable()
             dt.Load(rdr)
             ds = New DataSet
             ds.Tables.Add(dt)
             Return ds
       End Function

Calling these methods is therefore no different to calling BeginExecuteReader. For example the following code would work from both an .aspx page or an .ascx control.


    Dim db As Classes.Data.DAL
    Protected Sub Page_Load(ByVal sender As Object, ByVal e As System.EventArgs) Handles Me.Load
        'Append async attribute to connection string
        db = New Classes.Data.DAL(String.Concat _
        (ConfigurationManager.ConnectionStrings("MyDB")ConnectionString, ";async=true"))
        db.CommandText = "asyncTest"
        ' Launch data request asynchronously using page async task  
        Page.RegisterAsyncTask(New PageAsyncTask(New BeginEventHandler(AddressOf BeginGetData), _
        New EndEventHandler(AddressOf EndGetData), New EndEventHandler(AddressOf GetDataTimeout), _
        Nothing,True))
    End Sub
    Function BeginGetData(ByVal sender As Object, ByVal e As EventArgs, _
        ByVal cb As AsyncCallback, ByVal state As Object) As IAsyncResult
       Return db.BeginExecuteDataSet(cb, state)  
    End Function
    Sub EndGetData(ByVal ar As IAsyncResult)
        Try
           gv1.DataSource = db.EndExecuteDataSet(ar)
           gv1.DataBind()
        Catch ex As Exception
           lblMsg.Text = ex.ToString()
        Finally
           db.Dispose()
        End Try
    End Sub
    Sub GetDataTimeout(ByVal ar As IAsyncResult)
        db.Dispose()
        lblMsg.Text = "Async connection timed out"
    End Sub

There you have it, a DataSet returned asynchronously, from a Data Access Layer. Download the entire Data Access Layer (zip file contains both Visual Basic and C# versions - 3.05 kb).


Posted by Williarob on Wednesday, December 19, 2007 8:07 AM
Permalink | Comments (0) | Post RSSRSS comment feed