Do you want to know a secret? A deep, dark, dirty secret? One that, if revealed, would cause great angst in the ASP.NET community and prompt shouts of "Aha!" from the anti-Microsoft crowd?
Most Web sites I've seen built with ASP.NET aren't very scalable, not because of a flaw in ASP.NET, but because of how the technology is used. They suffer a self-imposed glass ceiling that limits the number of requests they can process per second. These sites scale just fine until traffic rises to the level of this invisible ceiling. Then throughput begins to degrade. Soon after, requests start to fail, usually returning "Server unavailable" errors.
The underlying reason has been discussed many times in MSDN®Magazine. ASP.NET uses threads from a common language runtime (CLR) thread pool to process requests. As long as there are threads available in the thread pool, ASP.NET has no trouble dispatching incoming requests. But once the thread pool becomes saturated-that is, all the threads inside it are busy processing requests and no free threads remain-new requests have to wait for threads to become free. If the logjam becomes severe enough and the queue fills to capacity, ASP.NET throws up its hands and responds with a "Heck no!" to new requests.
One solution is to increase the maximum size of the thread pool, allowing more threads to be created. That's the course developers often take when their customers report repeated "Server unavailable" errors. Another common course of action is to throw hardware at the problem, adding more servers to the Web farm. But increasing the thread count-or the server count-doesn't solve the issue. It just provides temporary relief to what is in reality a design problem-not in ASP.NET itself, but in the implementation of the actual site. The real problem for apps that don't scale isn't lack of threads. It's inefficient use of the threads that are already there.
A truly scalable ASP.NET Web site makes optimum use of the thread pool. That means making sure request-processing threads are executing code instead of waiting for I/O to complete. If the thread pool becomes saturated due to all the threads grinding away on the CPU, there's little you can do but add servers.
However, most Web apps talk to databases, Web services, or other external entities, and limit scalability by forcing thread-pool threads to wait for database queries, Web service calls, and other I/O operations to complete. A request targeting a data-driven Web page might spend a few thousandths of a second executing code and several seconds waiting for a database query to return. While the query is outstanding, the thread assigned to the request is unable to service other requests. That's the glass ceiling. And that's a situation you must avoid if you care about building highly scalable Web sites. Remember: when it comes to throughput, I/O is evil unless handled properly.
I/O isn't evil, though, if it doesn't gum up the thread pool. And ASP.NET supports three asynchronous programming models that act as anti-gumming agents. These models are largely unknown to the community, in part due to scant documentation. Yet knowing how-and when-to use them is absolutely essential to building cutting-edge Web sites.
Asynchronous Pages
The first, and generally most useful, of the three asynchronous programming models ASP.NET supports is the asynchronous page. Of the three models, this is the only one that's specific to ASP.NET 2.0. The others are supported all the way back to version 1.0.
I won't go into detail about asynchronous pages here because I did that in the October 2005 issue (msdn.microsoft.com/msdnmag/issues/05/10/WickedCode). The upshot is that if you have pages that perform relatively lengthy I/O operations, they're candidates to become asynchronous pages. If a page queries a database and the query takes, say, 5 seconds to return because it either returns a large amount of data or targets a remote database over a heavily loaded connection, that's 5 seconds that the thread assigned to the request can't be used for other requests. If every request behaved like this, the application would quickly get bogged down.
Figure 1 illustrates how an asynchronous page neatly solves this problem. When the request arrives, it's assigned a thread by ASP.NET. The request begins processing on that thread, but when the time comes to hit the database, the request launches an asynchronous ADO.NET query and returns the thread to the thread pool. When the query completes, ADO.NET calls back to ASP.NET, and ASP.NET grabs another thread from the thread pool and resumes processing the request.
Figure 1 Asynchronous Pages at Work (Click the image for a smaller view)
While the query is outstanding, zero thread pool threads are consumed, leaving all of the threads free to service incoming requests. A request that's processed asynchronously doesn't execute any faster. But other requests execute faster because they don't have to wait for threads to become free. Requests incur less delay in entering the pipeline, and overall throughout goes up.
Figure 2 shows the codebehind class for an asynchronous page that performs data binding against a SQL Server™ database. The Page_Load method calls AddOnPreRenderCompleteAsync to register begin and end handlers. Later in the request's lifetime, ASP.NET calls the begin method, which launches an asynchronous ADO.NET query and returns immediately, whereupon the thread assigned to the request goes back into the thread pool. When ADO.NET signals that the query has completed, ASP.NET retrieves a thread from the thread pool (not necessarily the same one it used before) and calls the end method. The end method grabs the query results and the remainder of the request executes as normal on the thread that executed the end method.
Not shown in Figure 2 is the Async="true" attribute in the ASPX's Page directive. This is required for an asynchronous page: it signals ASP.NET to implement the IHttpAsyncHandler interface in the page (more on this in a moment). Also not shown in Figure 2 is the database connection string, which includes an Async="true" attribute of its own so that ADO.NET knows to perform an asynchronous query.
AddOnPreRenderCompleteAsync is one way to structure an asynchronous page. Another way is to call RegisterAsyncTask. This has a couple of advantages over AddOnPreRenderCompleteAsync-the most important being that it simplifies the task of performing multiple asynchronous I/O operations in one request.
By: Jeff Prosise (http://msdn.microsoft.com/msdnmag/issues/07/03/WickedCode/)