序:這是一篇發表在2003年6月刊的MSDN Magazine的文章,現在已經不能線上閱讀,只提供chm下載。講的是非同步請求處理那些事,正是我上一篇博文涉及的東西(BTW,事實上這篇雜誌闡述了那麼搞然並卵),期間有搜索到這篇文章,很受裨益。擔心MS哪麽時候不再提供下載以及本地保管不便,所以現在謄上來 ...
序:這是一篇發表在2003年6月刊的MSDN Magazine的文章,現在已經不能線上閱讀,只提供chm下載。講的是非同步請求處理那些事,正是我上一篇博文涉及的東西(BTW,事實上這篇雜誌闡述了那麼搞然並卵),期間有搜索到這篇文章,很受裨益。擔心MS哪麽時候不再提供下載以及本地保管不便,所以現在謄上來,作為備份,方便日後回顧。
Fritz Onion
This article assumes you're familiar with C#, ASP.NET, and Multithreading
Level of Difficulty 1 2 3
Code download available at: Threading.exe (121KB)
SUMMARY
Fortunately for developers, threading in ASP.NET is a lot easier than it was in ASP. In this article, the author takes a look at threading in the ASP.NET HTTP pipeline, and explains how threads are managed efficiently without the involvement of the developer. The article considers how the common language runtime threadpool is used by ASP.NET to service requests, looks at the pooling mechanisms used for handlers, modules, and applications, and covers both IIS 5.0 and IIS 6.0 and how they differ in their approach to request processing and thread allocation. Finally, how and when to use asynchronous handlers is discussed for developers who still need to use threads in their own applications.
Contents
Threading in ASP.NET
Thread Dispatching in IIS 5.0 and 6.0
The Need for Asynchrony
Asynchronous Handlers
Asynchronous Handler with a Delegate
Asynchronous Handler with Custom Threads
Asynchronous Handler with a Custom Thread Pool
Asynchronous Pages
Conclusion
In traditional ASP, developers faced more threading issues than they knew what to do with. Because ASP was built on top of COM, there were very specific rules about the threading requirements of objects. Objects used by pages needed to be apartment threaded for maximum efficiency. Conversely, objects living in Session and Application state needed to be context-agile and able to aggregate the freethreaded marshaler, as well as have their state protected against concurrent access.
If you look at the documentation for ASP.NET, however, you will find very little information about threading requirements. Does this mean that all threading issues are solved and that ASP.NET developers can live carefree lives building .aspx pages and Microsoft? .NET Framework classes without cluttering their minds with concurrency issues? Well, yes, in most cases it does.
In this article, I will explore the details of threading in the HTTP pipeline and how it efficiently manages threads without burdening the developer. I will also take a look at how ASP.NET uses the common language runtime (CLR) thread pool to service requests, the pooling mechanisms used for handlers, modules, and applications, and some of the differences in request processing and thread allocation between Microsoft Internet Information Services (IIS) 5.0 and IIS 6.0. For those developers who just don't feel complete if they don't use threads in their applications, I will also introduce asynchronous handlers and discuss how and when to use them.
Threading in ASP.NET
To efficiently service multiple client requests, Web servers make extensive use of concurrency by launching multiple processes and/or spawning multiple threads to service requests and perform work. ASP.NET is no exception and uses multiple threads within each worker process to service requests. In spite of this fact, ASP.NET developers need not concern themselves with the multitude of issues that come with developing in a multithreaded environment. Page requests are always serviced on the same thread, and a distinct instance of the Page class is always created to service any new requests. Distinct instances of application and module objects are also used to service each request. It is important, however, to understand how threads are used to service requests so that you don't make incorrect assumptions about which objects in your applications may be accessed by concurrent threads.
To begin with, ASP.NET uses the process-wide CLR thread pool to service requests (for more background on the CLR thread pool, see the .NET column in this issue). The size of this pool is configurable in the processModel element of machine.config, and is set to a default of 25 worker threads and 25 I/O threads:
<processModel enable="true" ... maxWorkerThreads="25" maxIoThreads="25" />
Threads are processed differently depending whether you are using IIS 5.0 or IIS 6.0, which I will discuss shortly. For each request that comes in, a new instance of the appropriate HttpApplication-derived class is created, as are the associated modules for that app. To avoid reallocating apps and modules too often, each AppDomain maintains a pool of apps and modules. The maximum size of the app pool is the same as the size of the thread pool, so by default, up to 25 requests per worker process can be processed concurrently, each with its own app and module set. Figure 1 shows a snapshot in time of the ASP.NET worker process. In this scenario, there are two active apps in the worker process, each with a dedicated AppDomain. Each app is currently processing two requests, and each is using two threads from the CLR thread pool to service those requests.
Figure 1 Threading and Pooling in the HTTP Pipeline
Several aspects of this architecture may influence the way you construct your ASP.NET applications. First, the fact that applications and modules are instantiated multiple times means that you should never rely on adding fields or other states because they will not be shared across multiple requests, as you might think. Instead, use one of the many state repositories available in the pipeline, such as the application-wide cache, the session state bag, the appliction state bag, or the per-request Items collection of the HttpContext class. Additionally you can use static data if properly synchronized.
By default, most handlers created to service requests are not pooled. You can pool handlers and even control pooling on a per-handler basis through the IsReusable property of IHttpHandler, but the only handlers that are pooled implicitly are custom handlers that you write with no designated handler factory. The PageHandlerFactory class does not perform pooling, nor does the SimpleHandlerFactory class, which instantiates .ashx-defined handlers. Typically, therefore, each request is serviced by a freshly allocated instance of the appropriate handler class, which is discarded after the request completes.
Thread Dispatching in IIS 5.0 and 6.0
Requests in IIS 5.0 are typically serviced over I/O threads, or threads performing asynchronous I/O because requests are dispatched to the worker process using asynchronous writes to a named pipe. When the request is serviced in the worker process, it will typically use the thread on which the asynchronous read was performed. The threads used to service requests in the worker process are drawn from the process-wide CLR thread pool, although thread pool threads tied to I/O completion ports incorrectly return false from the IsThreadPoolThread property of the Thread class, so don't count on that property.
Although requests are typically serviced on I/O threads in IIS 5.0, under heavy loads, some requests are passed off to normal worker threads from the pool, so both types of threads are used simultaneously. The default size of the CLR thread pool is 25, with separate limits for I/O threads and worker threads. You can change the upper number of threads allocated in the CLR thread pool by modifying the processModel element found in machine.config, as was shown earlier.
Things change dramatically with IIS 6.0 (which ships with Windows? Server 2003). To begin with, inetinfo.exe is no longer used to service HTTP requests (although it remains in place to service other protocols, like FTP). Instead, HTTP requests are queued in a kernel-mode service, http.sys, which takes care of dispatching each request to the appropriate application queue. In addition, IIS 6.0 supports application pools, which are groupings of applications that will share a single worker process, now named w3pwp.exe. Application pools give you the option of having as many distinct worker processes as you want on any given server, depending on how much you care about process isolation between your Web applications.
This paradigm changes the way requests are handled in ASP.NET. Instead of dispatching requests from inetinfo.exe to the ASP.NET worker process, http.sys directly queues each request in the appropriate process. Thus all requests are now serviced by worker threads drawn from the CLR thread pool and never on I/O threads. Contrary to comments found in machine.config on Windows Server 2003, the processModel element is still used to initialize the upper bound of the thread pool, although all other attributes are ignored as they are now stored in the IIS 6.0 metabase.
The Need for Asynchrony
Many factors influence the scalability of a Web application. In general, any time multiple concurrent operations request exclusive access to a shared resource, the scalability of the system is jeopardized. High on the list of resources that are shared on a system is the CPU itself (or CPUs on a multiprocessor box). A server running ASP.NET has its CPU shared among concurrent requests by way of a thread pool. The purpose of the thread pool is to efficiently allocate CPU time for processing requests with an upper limit on the total number of threads allocated. This is an important restriction to have in place, as creating an unbounded number of threads can easily grind a system to a halt when a large number of requests are made concurrently.
However, in cases where threads from the thread pool are used to perform work that is not CPU-intensive (like making a request to a remote database or invoking a remote Web Service), it is possible to saturate the thread pool without high CPU utilization. In this scenario, the thread pool is in fact reducing the scalability of the system because requests may be delayed (or possibly rejected) even though the server was not busy processing other requests.
As an example of swamping the thread pool, consider the following ASP.NET page, slow.aspx, that artificially delays its response by two seconds (with non-CPU-bound activity) and prints out the thread on which it was serviced:
<!-- File: slow.aspx --> <%@ Page Language="C#" %> <%@ Import Namespace="System.Reflection" %> <%@ Import Namespace="System.Threading" %> <script runat="server"> protected void Page_Load(object src, EventArgs e) { System.Threading.Thread.Sleep(2000); Response.Output.Write("SlowResponse, threadid={0}", AppDomain.GetCurrentThreadId()); } </script>
Another ASP.NET page, fast.aspx, is nearly identical, except that it does not sleep for the two seconds that slow.aspx does:
<!-- File: fast.aspx --> <%@ Page Language="C#" %> <%@ Import Namespace="System.Reflection" %> <script runat="server"> protected void Page_Load(object src, EventArgs e) { Response.Output.Write("FastResponse, threadid={0}", AppDomain.GetCurrentThreadId()); } </script>
Using the Web Application Stress tool against a server hosting these two pages, you can test the average response time under heavy request traffic. An initial test issuing requests on 100 concurrent threads from a separate client machine to only the fast.aspx page results in an average response time of 0.0824 seconds, servicing 52,190 requests during a one-minute time interval. Running the client against both fast.aspx and slow.aspx, however, reduces the average response time for fast.aspx requests to 4.47 seconds (and slow.aspx is 6.54 seconds), with the total number of requests serviced in one minute reduced to 1062.
While the numbers are dramatically different across the board, the most disturbing result is that it took more than four seconds on average to serve fast.aspx, which you know takes very little time to process. Although contrived, this example shows the worst case scenario when you have pages that are relatively slow to serve fighting for the same request threads that more responsive pages contend for—a dramatic increase in response times. If the slow pages in your application are sluggish due to intensive CPU usage, there is not much you can do about it except to throw in more hardware. If, however, the slow pages in your application are slow because they are waiting on non-CPU-bound operations to complete, then the problem is not the lack of CPU power, but the fact that slow requests are saturating the thread pool so that other requests must be queued until a thread is released.
You have a few options if you discover that your system's scalability is being adversely affected by the ASP.NET thread pool. You could change the upper limit of the thread pool. As you have seen, the default upper limit is set to 25 worker threads and 25 I/O threads. Of course, increasing the upper bound in the thread pool is a somewhat coarse-grained solution to the problem. If you can find the magic number that keeps your CPU utilization high and only delays or denies requests when the server is truly too busy to process more requests, you are very fortunate. The number of threads necessary to keep the server truly busy is not static, but will fluctuate based on factors like demand and process complexity.
Another potential solution is to carefully identify the requests in your system that take longer to complete and perform non-CPU-bound operations and to allocate distinct threads to service them, freeing up the original thread pool thread to service additional requests. This brings me to asynchronous handlers.
Asynchronous Handlers
While most ASP.NET pages and handlers are serviced synchronously on threads drawn from the process-wide thread pool, it is possible to create handlers (and even pages) that service requests asynchronously. Asynchronous handlers implement the IHttpAsyncHandler interface, which derives from IHttpHandler implemented by synchronous handlers:
public interface IHttpAsyncHandler : IHttpHandler { IAsyncResult BeginProcessRequest(HttpContext ctx, AsyncCallback cb, object obj); void EndProcessRequest(IAsyncResult ar); }
Handlers that implement this interface must implement two additional methods besides the standard methods of IHttpHandler. The first, BeginProcessRequest, is called by the application class instead of directly calling the ProcessRequest. It is then up to the handler to launch a new thread to process the request and then immediately return from the BeginProcessRequest method, passing back a reference to a valid IAsyncResult instance so that the runtime knows the operation is complete. The other method, EndProcessRequest, is called when the request processing is complete and can be used to clean up any allocated resources if necessary.
Asynchronous Handler with a Delegate
If you have done any work with asynchronous execution in the .NET Framework, you know that the simplest and most often recommended way to perform work asynchronously is to use asynchronous delegate invocation. Invoking a delegate asynchronously by calling BeginInvoke implicitly uses a thread from the process-wide thread pool to execute the delegate function. Implementing an asynchronous handler using asynchronous delegate invocation is actually quite straightforward, as shown in Figure 2.
Figure 2 HTTP Handler Implementation
!-- File: AsyncDelegate.ashx --> <%@ WebHandler Language="C#" Class="EssentialAspDotNet.HttpPipeline.AsyncHandler" %> using System; using System.Web; using System.Threading; using System.Diagnostics; using System.Reflection; namespace EssentialAspDotNet.HttpPipeline { public delegate void ProcessRequestDelegate(HttpContext ctx); public class AsyncHandler : IHttpAsyncHandler { public void ProcessRequest(HttpContext ctx) { System.Threading.Thread.Sleep(2000); ctx.Response.Output.Write( "AsyncDelegate, threaded={0}", AppDomain.GetCurrentThreadId()); } public bool IsReusable { get { return true;} } public IAsyncResult BeginProcessRequest(HttpContext ctx, AsyncCallback cb, object obj) { ProcessRequestDelegate prg = new ProcessRequestDelegate(ProcessRequest); // Fire-and-forget return prg.BeginInvoke(ctx, cb, obj); } public void EndProcessRequest(IAsyncResult ar) { } } }
My implementation of IHttpAsyncHandler.BeginProcessRequest calls BeginInvoke on an instance of my ProcessRequestDelegate which has been initialized with a reference to my ProcessRequest method. This has the effect of executing ProcessRequest on a separate thread, distinct from the request thread.
Running the same stress test I performed earlier with my slow.aspx page, this time using my new asyncDelegate.ashx handler instead of fast.aspx, produces the following results. The average response time for fast.aspx is 4.14 seconds and 7.86 seconds for asyncDelegate.ashx, with the total number of serviced requests at 957.
Disappointingly, the response time did not really improve for requests made to fast.aspx. The reason for this is that asynchronous delegate invocation completely defeats the purpose of building an asynchronous handler because it draws from the same process-wide CLR thread pool that ASP.NET uses to service requests. While the primary request thread is indeed returned to the thread pool, another thread is taken out to perform the asynchronous delegate execution, which means that there is a net gain of zero threads for servicing additional requests, rendering the handler useless. You will run into the same problem if you use ThreadPool.QueueUserWorkItem to perform the asynchronous request processing because it also draws threads from the same process-wide CLR thread pool.
Asynchronous Handler with Custom Threads
To build a truly effective asynchronous handler, you must spawn an additional thread by hand in response to BeginProcessRequest. There are three important aspects to building a successful asynchronous handler. First, construct a class that supports IAsyncResult to return from BeginProcessRequest. Then, spawn the thread to perform your request processing asynchronously. Finally, notify ASP.NET that you are finished processing the request and are ready to return the response.
I will begin the construction of an asynchronous handler by building a class that supports IAsyncResult. This class is going to be returned from the call to BeginProcessRequest, and it will later be passed into my implementation of EndProcessRequest, so among other things, it is a useful place to store request-specific state that I may need to use during the processing of a request. The complete IAsyncResult interface to be implemented is shown here:
public interface IAsyncResult { public object AsyncState { get; } public bool CompletedSynchronously { get; } public bool IsCompleted { get; } public WaitHandle AsyncWaitHandle { get; } }
In this example, I'll store a reference to the HttpContext object associated with the request, a reference to the AsyncCallback delegate passed into BeginProcessRequest (which I'll later invoke to complete the request), and a generic object reference for extra data that may be used by the caller of BeginProcessRequest. The other element that must be implemented in this class is a synchronization object that threads can wait on to be signaled when the operation completes. I will use the common technique of supplying a ManualResetEvent that fires when my request is complete, but allocate it only if someone requests it. Finally, my class will have a convenience method called CompleteRequest, which will trigger the ManualResetEvent if it was created, invoke the AsyncCallback delegate, and set the IsCompleted flag to true. The complete class definition for AsyncRequestState is shown in Figure 3.
Figure 3 AsyncRequestState Class Definition
// From file: AsyncThread.ashx class AsyncRequestState : IAsyncResult { public AsyncRequestState(HttpContext ctx, AsyncCallback cb, object extraData ) { _ctx = ctx; _cb = cb; _extraData = extraData; } internal HttpContext _ctx; internal AsyncCallback _cb; internal object _extraData; private bool _isCompleted = false; private ManualResetEvent _callCompleteEvent = null; internal void CompleteRequest() { _isCompleted = true; lock (this) { if (_callCompleteEvent != null) _callCompleteEvent.Set(); } // if a callback was registered, invoke it now if (_cb != null) _cb(this); } // IAsyncResult interface property implementations public object AsyncState { get { return(_extraData); } } public bool CompletedSynchronously { get { return(false); } } public bool IsCompleted { get { return(_isCompleted); } } public WaitHandle AsyncWaitHandle { get { lock( this ) { if( _callCompleteEvent == null ) _callCompleteEvent = new ManualResetEvent(false); return _callCompleteEvent; } } } }
The next step is to spawn a new thread to process my request. The method I call on this new thread will need access to the state I cached in the AsyncRequestState class just shown. Unfortunately, the ThreadStart delegate used to spawn new threads in the .NET Framework does not take any parameters. To get around this, I create another class with the necessary state cached as data members (in this case, simply a reference to the AsyncRequestState object for this request) and with an instance method that can be used to initialize the ThreadStart delegate. This class, called AsyncRequest, is shown in Figure 4. Note that the ProcessRequest method I define in this class is the method that will be called from the manually created thread, and when it completes, it signals that the request processing is complete by invoking CompleteRequest on the AsyncRequestState object.
Figure 4 AsyncRequest
// From file: AsyncThread.ashx class AsyncRequest { private AsyncRequestState _asyncRequestState; public AsyncRequest(AsyncRequestState ars) { _asyncRequestState = ars; } public void ProcessRequest() { // This is where the non-cpu-bound activity would take place, such as // accessing a Web Service, polling a slow piece of hardware, or // performing a lengthy database operation. I put the thread to sleep // for two seconds to simulate a lengthy operation. Thread.Sleep(2000); _asyncRequestState._ctx.Response.Output.Write( "AsyncThread, {0}", AppDomain.GetCurrentThreadId()); // tell asp.net I am finished processing this request _asyncRequestState.CompleteRequest(); } }
Finally, I am ready to build the asynchronous handler class itself. This class, which I have called AsyncHandler, must implement all of the methods of the IHttpAsyncHandler interface shown earlier, which derives from IHttpHandler for a total of four methods. The ProcessRequest method is not used and will never be called because I implement BeginProcessRequest. In BeginProcessRequest, I create a new instance of the AsyncRequestState class, initializing it with the HttpContext object, the AsyncCallback delegate, and the generic object reference each passed in as parameters. I then prepare a new AsyncRequest object, initialized with the freshly created AsyncRequestState object, and launch a new thread. The implementation of EndProcessRequest will not do anything in this example, but it can be used in general to perform any cleanup or last-minute response additions. The AsyncHandler class definition is shown in Figure 5.
Figure 5 AsyncHandler Class Definition
// From file: AsyncThread.ashx public class AsyncHandler : IHttpAsyncHandler { public void ProcessRequest(HttpContext ctx) { // not used } public bool IsReusable { get { return false;} } public IAsyncResult BeginProcessRequest(HttpContext ctx, AsyncCallback cb, object obj) { AsyncRequestState reqState = new AsyncRequestState(ctx, cb, obj); AsyncRequest ar = new AsyncRequest(reqState); ThreadStart ts = new ThreadStart(ar.ProcessRequest); Thread t = new Thread(ts); t.Start(); return reqState; } public void EndProcessRequest(IAsyncResult ar) { // This will be called on the manually created thread in response to // calling the ASP.NET AsyncCallback delegate once the request has // completed processing. The incoming IAsyncResult parameter will be // a reference to the AsyncRequestState class, so the Context can be // accessed through that class. Note - you *cannot* access the // current context using the HttpContext.Current property, as you are // running on your own thread which has not been initialized with a // context reference. AsyncRequestState ars = ar as AsyncRequestState; if (ars != null) { // here you could perform some cleanup, write something else to the // Response, or whatever else you need to do } } }
When I build this handler and register it as an endpoint, as I did with the earlier handlers, it will successfully process requests asynchronously from the calling request thread of ASP.NET. Figure 6 shows the sequence of events that will take place when a request mapped onto the asynchronous handler is made. First the application class sees that my handler implements IHttpAsyncHandler, so instead of calling the synchronous ProcessRequest method, it invokes BeginProcessRequest on the handler, passing in the current context and an asynchronous callback delegate for completion call back. The handler then creates a new AsyncRequestState object, initialized with the parameters passed into BeginProcessRequest. Next, the handler creates a new AsyncRequest object, initialized with the AsyncRequestState object, and launches a new thread using the AsyncRequest.ProcessRequest method as the entry point. The handler then returns the AsyncRequestState object back to the application object and the calling thread is returned to the thread pool while the hand-created thread continues processing the request.
Figure 6 Async Handler Operation Phase 1
Once the ProcessRequest method of the AsyncRequest object is finished performing its lengthy tasks, it calls the CompleteRequest method of the AsyncRequestState class. This in turn will fire the AsyncCallback delegate originally passed into the BeginProcessRequest method, signaling that the response is prepared and ready for return. The first thing the AsyncCallback delegate will do is call the EndProcessRequest method on the asynchronous handler class. Once that returns, it will trigger the completion of the request by sending back the prepared response. All of this processing happens on the secondary thread that I created in the handler, not on a thread pool thread. Figure 7 shows the steps involved in completing the request to the asynchronous handler.
Figure 7 Async Handler Operation Phase 2
When I ran the same stress test I performed earlier with the slow.aspx page, this time using the new AsyncThread.ashx handler instead of AsyncDelegate.aspx, the following results are produced. The average response time for fast.aspx is .012 seconds and 2.014 seconds for AsyncThread.ashx, with the total number of serviced requests at 6132.
The average response time for requests made to fast.aspx is much improved because the thread pool is not tied up servicing requests to the slow handler.
Asynchronous Handler with a Custom Thread Pool
There is one remaining problem with this asynchronous handler implementation—it has the potential to create an unbounded number of threads. If a large number of requests are made to the asynchronous handler, all of which take a significant amount of time to service, then you could easily end up creating more threads than the underlying operating system can handle. To deal with this, I need to provide a secondary thread pool to service the requests in a bounded fashion. Details of creating custom thread pools are beyond the scope of this article, but a full example written in C# by Mike Woodring is available for download at http://www.bearcanyon.com/dotnet/threadpool.zip. This custom thread pool also supports propagation of call context and HttpContext, which means that you can access HttpContext.Current in the asynchronous method, unlike the previous example. The class implementing the thread pool is called DevelopMentor.ThreadPool, and its constructor takes an initial thread count, a maximum thread count, and a string to give the pool a name. To issue asynchronous requests with this pool, call PostRequest and pass in a reference to a WorkRequestDelegate.
To rewrite the asynchronous handler to use this custom thread pool there will be two major changes that just spawned new threads. First, add a static member of type DevelopMentor.ThreadPool to the handler class and a static constructor to initialize the thread pool once per AppDomain. Then change the implementation of BeginProcessRequest to call PostRequest on the thread pool with a new instance of WorkRequestDelegate (see Figure 8). If you run the same stress test performed earlier with the slow.aspx page, this time using the new AsyncPool.ashx handler instead of AsyncThread.aspx, you get the following results. The average response time for fast.aspx is .055 seconds and 12.24 seconds for AsyncThread.ashx with the total number of serviced requests at 1042.
Figure 8 Calling PostRequest
<!-- File: AsyncPool.ashx --> <%@ WebHandler Language="C#" Class="EssentialAspDotNet.HttpPipeline.AsyncHandler" %> namespace EssentialAspDotNet.HttpPipeline { // AsyncRequestState and AsyncRequest remain the same // as in the previous example public class AsyncHandler : IHttpAsyncHandler { static DevelopMentor.ThreadPool _threadPool; static AsyncHandler() { _threadPool = new DevelopMentor.ThreadPool(2, 25, "AsyncPool"); _threadPool.PropogateCallContext = true; _threadPool.PropogateThreadPrincipal = true; _threadPool.PropogateHttpContext = true; _threadPool.Start(); } public void ProcessRequest(HttpContext ctx) { // not used } public bool IsReusable { get { return false;} } public IAsyncResult BeginProcessRequest(HttpContext ctx, AsyncCallback cb, object obj) { AsyncRequestState reqState = new AsyncRequestState(ctx, cb, obj); _threadPool.PostRequest( new DevelopMentor.WorkRequestDelegate(ProcessRequest), reqState); return reqState; } public void EndProcessRequest(IAsyncResult ar) { } void ProcessRequest(object state, DateTime requestTime) { AsyncRequestState reqState = state as AsyncRequestState; // Take some time to do it Thread.Sleep(2000); reqState._ctx.Response.Output.Write( "AsyncThreadPool, {0}", AppDomain.GetCurrentThreadId); // tell asp.net you are finished processing this request reqState.CompleteRequest(); } } }
Note that the average response time for requests made to fast.aspx is still minimal, and there is no longer the danger of creating an unbounded number of threads. There is some additional management involved with maintaining a second thread pool, which results in the extended request times for the slow handler.
Asynchronous Pages
So far, all of the examples of asynchronous request handling have involved building custom handlers (in this case, using .ashx files). There will be many cases where you can identify normal pages (.aspx) in your ASP.NET applications that are also good candidates for asynchronous execution because they perform non-CPU-bound tasks that take significant amounts of time. Such pages are also potential bottlenecks for the ASP.NET thread pool. The Page class itself implements the IHttpHandler interface, and thus services requests synchronously. To change a given .aspx page to service requests asynchronously, it is necessary to implement IHttpAsyncHandler in the Page-derived class and then use the techniques described in this article to invoke the ProcessRequest method of the Page class on an alternate thread.
One convenient way to make any page asynchronous is to create an alternate base class from which the primary page can derive and then specify this alternate base class as the base class from which the page should derive using the Inherits attribute of the @Page directive. The AsyncPage class definition shown in Figure 9 defines such a base class. Note that this implementation uses the custom thread pool shown earlier to service requests to the page asynchronously. This implementation of BeginProcessRequest invokes the PostRequest method of the thread pool with a new WorkRequestDelegate initialized with a reference to my overload of ProcessRequest. In the overload, I invoke the base class scope of ProcessRequest (with the original signature as defined in the IHttpHandler interface), which performs the standard request processing of a page, only this time it will be performed on the thread pool thread instead of the primary request thread (see Figure 9).
Figure 9 AsyncPage
namespace EssentialAspDotNet.HttpPipeline { public class AsyncPage : Page, IHttpAsyncHandler { static protected DevelopMentor.ThreadPool _threadPool; static AsyncPage() { _threadPool = new DevelopMentor.ThreadPool(2, 25, "AsyncPool"); _threadPool.PropogateCallContext = true; _threadPool.PropogateThreadPrincipal = true; _threadPool.PropogateHttpContext = true; _threadPool.Start(); } public new void ProcessRequest(HttpContext ctx) { // not used } public new bool IsReusable { get { return false;} } public IAsyncResult BeginProcessRequest(HttpContext ctx, AsyncCallback cb, object obj) { AsyncRequestState reqState = new AsyncRequestState(ctx, cb, obj); _threadPool.PostRequest( new DevelopMentor.WorkRequestDelegate(ProcessRequest), reqState); return reqState; } public void EndProcessRequest(IAsyncResult ar) { } void ProcessRequest(object state, DateTime requestTime) { AsyncRequestState reqState = state as AsyncRequestState; // Synchronously call base class Page.ProcessRequest // as you are now on a thread pool thread. base.ProcessRequest(reqState._ctx); // Once complete, call CompleteRequest to finish reqState.CompleteRequest(); } } }
As an example of using this new asynchronous page base class, consider the following ASPX page that inherits from the AsyncPage class. This base class could be applied to any ASPX page in a similar fashion to turn it into an asynchronously executed page:
<!-- AsyncPage.aspx --> <%@ Page Language="C#" Inherits="EssentialAspDotNet.HttpPipeline.AsyncPage" %> <%@ import Namespace="System.Reflection" %> <%@ import Namespace="System.Threading" %> <script runat="server"> protected void Page_Load(object src, EventArgs e) { System.Threading.Thread.Sleep(2000); Response.Output.Write("AsyncPage, {0}", AppDomain.GetCurrentThreadId); } </script>
The results of performing the stress test with fast.aspx and the asynchronous page, AsyncPage.aspx, are as follows with results matching those of the asynchronous custom handler using a custom thread pool. The average response time for fast.aspx is .054 seconds and 11.92 seconds for AsyncPage.aspx, with the total number of serviced requests at 965.
Conclusion
Building asynchronous handlers instead of synchronous ones actually adds additional overhead to request processing, so you should take the time to determine if the asynchronous capability of these types of handlers is really what you need. The purpose of an asynchronous handler is to free up an ASP.NET thread pool thread to service additional requests while the handler is processing the original request. This only makes sense if the work of servicing the request requires a significant amount of non-CPU-bound time to complete. For example, if the completion of a request depends on the completion of a number of remote procedure calls, or perhaps Web Service invocations, that would be a candidate to implement an asynchronous handler. Building asynchronous handlers to service CPU-intensive requests only adds threads to compete with the ASP.NET thread pool threads and may actually slow down the overall processing time for the request.
For related articles see:
The ASP Column: HTTP Modules
HTTP Pipelines: Securely Implement Request Processing, Filtering, and Content Redirection with HTTP Pipelines in ASP.NET
IIS 6.0: New Features Improve Your Web Server's Performance, Reliability, and Scalability
For background information see:
ASP.NET QuickStart Tutorial
Building Secure ASP.NET Applications: Authentication, Authorization, and Secure Communication
Fritz Onionis on the technical staff at DevelopMentor, where he researches, teaches, and writes courses on the .NET Framework including Essential ASP.NET. This article is adapted from Essential ASP.NET with Examples in C# (Addison-Wesley, 2003). A second edition, Essential ASP.NET with Examples in Visual Basic .NET, is available for those averse to semicolons.
- EOF -