Freigeben über


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.

Comments

  • Anonymous
    June 23, 2008
    The comment has been removed
  • Anonymous
    June 23, 2008
    Brock,Great points! I was writing this post with my fingers crossed (can you write like anyway?....LOL). I had mixed feelings just because of the points you call out...without delving into the details I needed to provide the solution to the application written in netfx 1.1Refer  to "Asynchronous Pages in ASP.NET 1.x"athttp://msdn.microsoft.com/en-us/magazine/cc163725.aspxI needed something simpler than what they describe there.Narrowing my scenario... the customer needed to provided parallel work for the web site that is not under heavy load but needed faster response time. So having that in mind i thought the solution is suitableIs it?
  • Anonymous
    June 30, 2008
    I've had a similar requirements (not exactly the same as you state, but close enough) in the past and my approach was to use a custom thread pool to address the issue #1 I raised above. The implementation I used was from Mike Woodring at bearcanyon.com and it's still in production. Another reason I wanted to manually use a custom thread pool was because many of the built-in async APIs from the CLR use the TP internally which doesn't help the saturation problem.As I always like to say: Programming is hard. :)
  • Anonymous
    June 30, 2008
    The comment has been removed
  • Anonymous
    July 06, 2008
    So, the conclusion is that we should always use async pattern as described inhttp://msdn.microsoft.com/en-us/magazine/cc163725.aspxrather than threadpool objects or multithreading ?Madhur
  • Anonymous
    July 06, 2008
    "Always" us very broad.Mark Twain would say: "All generalization is wrong, including this one".My take - seek the way to avoid using async and threading at all costs. Use it as the last resort.WHat's your scenario?
  • Anonymous
    July 06, 2008
    I have a webpart based ASP.NET portal in which webpart usually contains datagrid or graphs. All the displayed webparts are connected to one hidden datawebpart which supplies data to them trough webpart connections (dataset wrapped by interface).On couple of pages, with lots of webparts, the performance is going down. So I was planning to nail down this problem by async. Since webpart connections would be executed synchronously when they could be done in async .
  • Anonymous
    July 08, 2008
    oh, unfortunately i am not in the know wrt web parts development.Sorry about that.