ASP.NET Threading Nasties
In our System.Threading talk we cover the impact of threading in an ASP.NET application and specifically how damaging to your applications scaleability it can be to call external systems with unpredictable performance characteristics. (eg. SQL-Server, WebServices, COM component, java, etc).
The problem here is you will be "hogging" your ASP.NET thread for the duration of the transaction. You may not feel this is a problem because you clearly can't return the page until this external transaction has completed so you may as well wait. You might even turn this into an async call so you can do something else in parallel however you still end up ultimately waiting for the remote system to complete before you can complete your page construction and return the page to the caller.
So why is this so bad?
Well the problem is you have a finite number of threads available in the Thread Pool to process page requests so if you are "hogging" them in your pages while you wait for an external system to respond you will be blocking other page requests.
To demonstrate this I knocked up two aspx pages; one which did nothing and another which spun up a new thread which waited for 30 seconds before completing.
When I ran my nothing page alone using ACT over 5 mins I got the following results:
Response Code: 200 - The request completed successfully. Count: 141,575 Percent (%): 100.00
When I ran my nothing page and my badly threaded page at the same time using ACT over 5 mins I got the following results (I started my thread page first which allowed it to grab all the available threads!):
Response Code: 503 - The service is temporarily overloaded. Count: 45,169 Percent (%): 99.92
Response Code: 500 - The server encountered an unexpected condition that prevented it from fulfilling the request. Count: 36 Percent (%): 0.08
So as you can see from the results above my badly threaded page completely blocked all activity on the web server for 5 whole minutes!
It is also worth noting that the processor utilisation during this period was almost zero so the web server was genuinely busy doing nothing!
I have included the source code for my badly threaded page below. In my example I use "raw" threads however remember the same applies to async calls to SQL Server, Web Services, etc. I think this is particularly important when using 3rd party Web Services because your partners behaviour will have a direct impact on yours. You need to protect yourself from a scenario where your partner's response times drops through the floor or they become unavailable. Remember this effect can be chained so they might be busy waiting on someone else!
So finally what is the solution? Well Fritz Onion has written a couple of really good articles on this both for ASP.NET 1.1 and ASP.NET 2.0 so there is no point in my repeating his excellent advice.
These articles can be found at:
Use Threads and Build Asynchronous Handlers in Your Server-Side Web Code (ASP.NET 1.1)
Async Pages in ASP.NET 2.0 - some results
The moral of this story; Be careful what code you write - I might just kill your web server!
The source code for my busy page is below.
public class WebForm1 : System.Web.UI.Page
{
Thread _t;
DateTime _start;
protected System.Web.UI.WebControls.Label Label1;
private void Page_Load(object sender, System.EventArgs e)
{
_start = DateTime.Now;
_t = new Thread(new ThreadStart(MyThread));
_t.Start();
}
private void MyThread()
{
string setting = ConfigurationSettings.AppSettings["ThreadWait"];
int wait = int.Parse(setting);
Thread.Sleep(wait);
}
protected override void OnPreRender(EventArgs e)
{
base.OnPreRender (e);
_t.Join();
Label1.Text = "Started - " + _start.ToLongTimeString()+"
";
Label1.Text += "Finished - " + DateTime.Now.ToLongTimeString();
}
#region Web Form Designer generated code
override protected void OnInit(EventArgs e)
{
//
// CODEGEN: This call is required by the ASP.NET Web Form Designer.
//
InitializeComponent();
base.OnInit(e);
}
///
/// Required method for Designer support - do not modify
/// the contents of this method with the code editor.
///
private void InitializeComponent()
{
this.Load += new System.EventHandler(this.Page_Load);
}
#endregion
}