Async Pages API

Published 19 October 05 09:48 AM | despos

Chapter 5 of Programming Microsoft ASP.NET 2.0 Applications--Advanced Topics is tentatively titled "Building Richer Pages" and discusses three topics that can deserve a (short) chapter themselves--async pages, custom $-expressions, and page parser filters. These are apparently unrelated but used together offer you an unprecedented power as far as the runtime behavior of the page is concerned. You can make the page work async, expand your macros at parse time, build your own list of safe controls (like SharePoint does).

 

When I started on async pages, guess what, the first thing I did was looking into the excellent Jeff's article on MSDN Magazine and a number of posts from Fritz. It went pretty good and smooth until I tackled the RegisterAsyncTask method on the Page class.

 

In brief, to build an async page you have two steps to accomplish:

 

  1. Add Async=true to your @Page directive
  2. Register the async task(s) to execute

#2 can be done in either of two (nonexclusive) ways: using AddOnPreRenderCompleteAsync or RegisterAsyncTask. Both are new methods on the Page class. The former adds a pair of Begin/End async event handlers to the PreRenderComplete event. The latter requires you to pack the same pair of handlers (and more actually) in a wrapper class--PageAsyncTask--to be passed as an argument to RegisterAsyncTask. The tasks execute in between PreRender and PreRenderComplete on distinct threads--typically, the Begin thread is an ASP.NET worker thread and the End thread comes from completion thread pool.  

 

Functionally speaking you get pages working exactly in the same way. Is there any difference? And why two distinct APIs? Here are some thoughts and findings:

  

  • RegisterAsyncTask is a kind of independent API for executing tasks async from within an ASP.NET page. Unlike AddOnPreRenderCompleteAsync, it doesn't strictly require Async=true, though
  • RegisterAsyncTask works also with Async=false (at least in Beta 2). The original thread that started the Begin phase is blocked until all operations are complete. If you use tracepoints (coolest feature ever of VS2005 that I found out about only yesterday) you see that the thread of Page_Load is the same of Page_Unload.
  • For obvious scalability reasons, you might typically want to use RegisterAsyncTask in conjunction with async=true. However, the API is somewhat independent from async pages
  • AddOnPreRenderCompleteAsync targets a multicast delegate so you can repeatedly call it to register multiple tasks. All these tasks execute sequentially to guarantee that the page renders only when all tasks are complete. Using tracepoints you see Begin/End Begin/End Begin/End sequences for each registered task. The thread on End triggers the next Begin. There’s no attempt of getting some parallelism
  • If you want to obtain parallel execution with AddOnPreRenderCompleteAsync you must write your own IAsyncResult—which is boring but not hard (also thanks to a couple of posts from Dmitri). You use one Begin/End pair and fire internally all required async tasks.  
  • For one single async task, using RegisterAsyncTask (with Async=true) or AddOnPreRenderCompleteAsync is the same
  • For multiple simultaneous tasks, you either use multiple calls to RegisterAsyncTask (with Async=true) or one call to AddOnPreRenderCompleteAsync with a custom IAsyncResult object to track the termination of all tasks.
  • Using RegisterAsyncTask (with Async=true) makes your life considerably easier and results in simpler code to write

 

Using RegisterAsyncTask has also some other plusses:

  

  • The End thread enjoys a richer context. It has impersonation info, intrinsics, HTTP context. For example, if you registered the task using AddOnPreRenderCompleteAsync, you can't rely on integrated security to access a database in the End handler. Likewise, you can't use HttpContext.Current to trace or access intrinsics
  • Timeout support. You can define a timeout handler which runs if any of the tasks is not complete after a number of seconds (45 by default)
  • Possibility of parallel execution (parallel execution is not the default, though)
  • Much simpler (and I’d say cleaner) programming model for multiple tasks
  • You can use the ExecuteRegisteredAsyncTasks method (on Page) to start tasks on demand. For example, in a postback without waiting for the unwind point between PreRender and PreRenderComplete

The bottom line?

 

  • Use AddOnPreRenderCompleteAsync for one task, opt for RegisterAsyncTask for multiple tasks   

 PS: I'm aware that a lot of details about async pages are missing here (i.e., what the heck the unwind point is?). For an overview (and more) read the Jeff's article on MSDN Magazine.

 

Comments

# Guy said on October 19, 2005 08:31 AM:

But what about the simple case of launching a fire-and-forget Web Service from an ASP.NET page. Currently marking the Web Service with the oneway=true attribute does not propogate the thread context (making database connection impossible). Waiting for the end method or an asynchronous call holds up the page. Indigo seems to be one future possibility.

Now that threads are released, is it OK to leave the page hanging for an empty end method.

Is there a good 1.1 solution and does the new MethodAsync methods and MethodCompleted events help with this problem.

Guy

# DinoE said on October 19, 2005 09:00 AM:

As far as I know, there's no explicit support for the fire-and-forget approach. If you try to set the End handler to null you get a run time exception. You can use an async page and use an empty End handler.

As for MethodAsync/MethodCompleted, the good news is that the thread context is richer than usual and includes impersonation info as well as the HTTP context so that you can trace and access intrinsics as usual.

For 1.x, I'm afraid you have to implement async pages yourself without the facilities of ASP.NET 2.0. There's an excellent article by Fritz Onion on MSDN Magazine. Once you have an async handler for the page you fire the Web service on an AsyncCallback with an empty End handler (empty if you want it to be fire-and-forget).

# Guy said on October 20, 2005 09:00 AM:

My impression is that if you use an empty End handler in 1.x for a long running process, it holds a server thread, which will quickly deplete the ASP thread pool.

In 2.x it seems like there might be hope, with waiting pages giving back there thread to the pool.

Currently I'm just calling a sync WebService that starts its own thread and immediately returns, leaving me to manage my own threads. If I can find the time I will try a custom thread pool.

It just seems amazing to me that something a simple as fire-and-forget is so difficult.

# DinoE said on October 20, 2005 11:37 AM:

I could have been confused in my last reply. What's the problem if you just use a sync web service call for a OneWay method? The Web service should reply immediately in this case and you can go on. If waiting for this simple answer is too much you might come up with an async page in 2.0 and queue the request to the thread pool in 1.x.

You're absolutely right in your first statement about using End in 1.x--your thread will wait unless you make hte page asynchronous--with manual code

# Guy said on October 20, 2005 01:34 PM:

The OneWay Web service has a bug/feature that it doesn't preserve the security context, so integrated security to the database fails

Login failed for user '(null)'. Reason: Not associated with a trusted SQL Server connection.

If you "queue a request" with ThreadPool.QueueUserWorkItem it will also steal threads from the server and I think there may also be a problem with Thread.IsBackground being set to true causing the thread to disapear if run from a webservice (I am not certain about this)

# Alexander said on October 26, 2005 08:16 AM:

see:
http://www.eggheadcafe.com/articles/20050818.asp
http://www.eggheadcafe.com/articles/20040313.asp

adobr at gmx.de

# Guy said on October 26, 2005 09:13 PM:

I tried this, as well as another asych Fire and Forget class based on invoke. But I think there are problems with both methods. I'm pretty sure that they both steal threads from the ASP.NET pool and use a thread priority and Thread.IsBackground set to true that cannot be changed.

Thread.IsBackground means that if the calling thread goes away the thread is aborted, and I did find that long running threads disappeared when called from a Web service. Some of this is speculation but according to


http://pluralsight.com/blogs/craig/archive/2005/10/14/15574.aspx
it has had its IsBackground property set to true. When you do this, you're saying, "It's okay if the process shuts down while this thread is still running."

http://msdn.microsoft.com/msdnmag/issues/05/10/WickedCode/default.aspx
"you should not launch asynchronous operations that borrow from the same thread pool that ASP.NET uses. For example, calling ThreadPool.QueueUserWorkItem"

Sorry for hijaking this blog

# Guy said on October 27, 2005 07:39 AM:

I have started a new thread on my own <a href="http://lukesg.blogspot.com/2005/10/problems-with-async-fire-and-forget.html">blog</a>.

Leave a Comment

(required) 
(required) 
(optional)
(required)