Improve ASP.NET Performance With Multithreading Using Thread Or ThreadPool Objects

Your ASP.NET application performs slower than expected? How many times do you access your database for each request? Did you use SQL Profiler to find out? Did you know that chatty database access is one of the most common performance sins (this is my own observation)?

Want to improve? - Reduce the number of database queries.

Not an option? - You may want using multithreading. Proceed with caution.

Real world scenario

You need to run 3 independent heavy database queries – Q1, Q2, Q3. You need to use the result from the three – Result = Q1+Q2+Q3. You never know the order in which each query completes. It can be: Q1Q2Q3, Q1Q3Q2, Q2Q3Q1, Q2Q1Q3, Q3Q2Q1, Q3Q1Q2. Missed any combination? Run one after another would create significant latency. Running each on its own thread would create latency of Max(Q1,Q2,Q3) which is less that sum of the three.

How to run each query on its own thread and wait for the completion for each one?

Using Thread object

The following code spawns three threads and waits for all to join, and then use the results from each.

    1: Thread t1 = new Thread(DoWork1);
    2: t1.Start();
    3:  
    4: Thread t2 = new Thread(DoWork2);
    5: t2.Start();
    6:  
    7: Thread t3 = new Thread(DoWork3);
    8: t3.Start();
    9:  
   10: t1.Join(1000);
   11: t2.Join(1000);
   12: t3.Join(1000);
   13:  
   14:  
   15: Label1.Text = (i1 + i2 + i3).ToString();

See any issues using it in ASP.NET? One caveat though, creating threads manually like this may cause performance hit as it involves context switching – CPU consuming operation. Consider using ThreadPool object.

Using ThreadPool object (preferred, but are you using COM?)

ThreadPool is preferred as it already has live threads allocated for you. No need to ask for a favor from CPU in the moment of truth. Here is the code:

    1: public partial class _Default : System.Web.UI.Page
    2: {
    3:     int i1 = 0;
    4:     int i2 = 0;
    5:     int i3 = 0;
    6:  
    7:     WaitHandle[] waitHandles = new WaitHandle[]
    8:         { 
    9:             new ManualResetEvent(false),
   10:             new ManualResetEvent(false),
   11:             new ManualResetEvent(false) 
   12:         };
   13:  
   14:  
   15:     protected void Page_Load(object sender, EventArgs e)
   16:     {
   17:  
   18:     }
   19:  
   20:     protected void Button1_Click(object sender, EventArgs e)
   21:     {
   22:         Stopwatch sw = new Stopwatch();
   23:  
   24:         sw.Start();
   25:  
   26:         WaitCallback method1 = new WaitCallback(DoWork1);
   27:         bool isQueued1 = ThreadPool.QueueUserWorkItem(method1, waitHandles[0]);
   28:  
   29:         WaitCallback method2 = new WaitCallback(DoWork2);
   30:         bool isQueued2 = ThreadPool.QueueUserWorkItem(method2, waitHandles[1]);
   31:  
   32:         WaitCallback method3 = new WaitCallback(DoWork3);
   33:         bool isQueued3 = ThreadPool.QueueUserWorkItem(method3, waitHandles[2]);
   34:  
   35:         if (WaitHandle.WaitAll(waitHandles, 5000, false))
   36:             Label1.Text = (i1 + i2 + i3).ToString();
   37:         else
   38:             Label1.Text = "Problem";
   39:  
   40:     }
   41:  
   42:      void DoWork1(object state)
   43:     {
   44:         int.TryParse(TextBox1.Text, out i1);
   45:  
   46:         //HEAVY QUERY GOES HERE. Sleep is for the demo only! Remove it!
   47:         Thread.Sleep(i1);
   48:         ManualResetEvent mre = (ManualResetEvent)state;
   49:         mre.Set();
   50:  
   51:     }
   52:  
   53:      void DoWork2(object state)
   54:     {
   55:         int.TryParse(TextBox2.Text, out  i2);
   56:  
   57:         //HEAVY QUERY GOES HERE. Sleep is for the demo only! Remove it!
   58:         Thread.Sleep(i2);
   59:         ManualResetEvent mre = (ManualResetEvent)state;
   60:         mre.Set();
   61:     }
   62:  
   63:      void DoWork3(object state)
   64:     {
   65:         int.TryParse(TextBox3.Text, out  i3);
   66:  
   67:         //HEAVY QUERY GOES HERE. Sleep is for the demo only! Remove it!
   68:         Thread.Sleep(i3);
   69:         ManualResetEvent mre = (ManualResetEvent)state;
   70:         mre.Set();
   71:     }
   72:  
   73: }

Caveat

If your function calls on COM object – avoid using ThreadPool, its Apartment model is not compatible with COM and it cannot be changed. More info - Pitfalls With .Net Multithreading And COM Objects – Threads Must Have Compatible Apartment Models (MTA vs. STA).

Another point to call out is that performance improvements based on multithreading is subject to amount of available CPU’s. Test your solution first!

This post was written with help from Lior, MCS Israel Architect.

Download sample project with the code from my SkyDrive:

Enjoy.