Amazon.com Widgets All posts tagged 'threadpool'

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