This article may contain URLs that were valid when originally published, but now link to sites or pages that no longer exist. To maintain the flow of the article, we've left these URLs in the text, but disabled the links.
September 1998
Don Box is a co-founder of DevelopMentor where he manages the COM curriculum. Don is currently breathing deep sighs of relief as his new book, Essential COM (Addison-Wesley), is finally complete. Don can be reached at http://www.develop.com/dbox/default.asp. |
Developers are increasingly using the Active Server Pages (ASP) runtime environment for COM components. Since its inception, ASP has had a reputation for being quirky with respect to threading. One reason for this behavior is that these pages run within an ISAPI extension DLL, which has its own threading problems. Another reason is that some performance tips currently in circulation are totally bogus. While each version of ASP has its own idiosyncrasies, I will narrow my discussion to the version of ASP that ships with Microsoft® Internet Information Server (IIS) 4.0.
ASP generally follows the rules of COM. If you write an in-process component that is accessed by an ASP script, ASP won't do nasty things to your object. However, if you make it hard for ASP to use your object, overall application performance will suffer. An ASP application is composed of one or more ASP files under a single virtual directory. ASP files contain embedded server-side scripts that are parsed by IIS before it sends the response back to the client. At runtime, each ASP application has one or more active sessions that correspond to a particular Web user. A single global.asa file allows the ASP developer to define application and session-wide event handlers and objects. Consider the following global.asa file: |
|
This script defines empty handlers for each of the four intrinsic events that are fired by the ASP runtime. A global.asa file can also contain <object> tags that allow objects to be created with application, session, or page scope. This global.asa fragment |
|
creates one instance of the COM class Animals.Cat that is associated with all sessions of the application. It also associates a unique instance of the COM class Animals.Dog with every session of the application.
ASP postpones creating these objects until their programmatic names are referred to in script text. To access the application-wide Cat, you can simply write the following line of script in an ASP page: |
|
To access the Dog that is created specifically for this session, you can write the following line of ASP script: |
|
Note that each Web client will have its own private server-side Dog object, while all Web clients will share access to the server-side Cat.
Most of the interesting bits of an ASP application do not happen in a global.asa, but in individual ASP files that clients browse to within their Web browsers. ASP pages are standard HTML-based Web pages that also contain server-side scripts and objects. To embed a server-side script into an ASP page, you can either use the RunAt attribute |
|
or the <% and %> delimiters: |
|
Objects that are specific to an ASP page can be created using <object> tags |
|
or by dynamically using the intrinsic ASP Server object from an ASP script: |
|
Technically, you can create objects using the VBScript intrinsic CreateObject, but that's considered poor style.
It is also possible (at least in theory) to associate objects dynamically with the current session or application. |
|
Once associated with the session or application objects, these references are visible across multiple invocations of the ASP pages in an application: |
|
ASP requires the client to consent to using HTTP cookies in order to associate the session object with the appropriate client request. Web applications can disable session management either on an application or page-wide basis.
ASP Script Elements and COM Threading
|
Figure 1 ASP and Threads |
If session state is disabled for the application (by using the IIS administration tools) or the particular ASP page (by using the @EnableSessionState directive), IIS makes no attempt to serialize requests from a single Web client. Each sessionless request is simply queued on a random thread in the pool where each of the embedded scripts will be processed sequentially as the resulting HTML is shipped back to the client. It is illegal to access the session object (or any global.asa objects with session scope) from a sessionless ASP page. If a single client has several open browser windows to a sessionless ASP page, it is possible that multiple refreshes from the client will execute concurrently.
If session state is enabled (the default setting), IIS serializes concurrent requests within a single ASP session. For this reason, the intrinsic session object does not have Lock and Unlock methods (as does the intrinsic application object). Suppose a single client has several browser windows open to a session-enabled ASP application. ASP ensures that any requests that are issued concurrently are processed sequentially. However, it is possible that multiple sessions (which implies multiple clients) can execute concurrently within an ASP application. This is why the intrinsic application object (which is visible to all sessions) has Lock and Unlock methods that allow ASP scripts to serialize updates to the shared state. Of course, holding the application object locked for any period of time can cause massive performance problems due to contention. |
Figure 2 No Thread Affinity |
By default, the pages within an ASP session do not have thread/apartment affinity. Hence, ASP requests from a particular client may be dispatched to different threads over the lifetime of the session. This affords the ASP plumbing flexibility in terms of dispatching requests to available or idle threads. To avoid having a single client consuming multiple server-side threads, the ASP runtimes keep track of which requests have been dispatched to each of the pooled threads. In particular, the ASP runtimes ensure that requests from a particular session never span more than one thread simultaneously. Thus, the situation shown in Figure 2 cannot happen. Instead, after dispatching a request from a given session, ASP detects subsequent requests from the same session and ensures that they are dispatched to the same thread queue, as shown in Figure 3.
While IIS tries to keep a particular session on one thread while requests for the session are pending on the Web server, IIS does not normally try to keep a session on a particular thread/apartment across session idle times. If a particular client is constantly submitting requests in parallel within the same session, the requests will stick to the same thread. However, when the client stops to read a Web page, a different thread may process the next request. Therefore, the thread that was used to process the previous request may be busy processing a request from another user (in another session), but ASP may have other threads that are currently idle and more than happy to service the request. The Application_OnStart and Session_OnStart event handlers in a global.asa file are run on the same thread that processes the first page of the application/session. In this respect, they are logically prepended to the first ASP page in a particular session. |
Figure 3 Thread Affinity |
IIS 4.0 allows ASP applications to be configured to run as in-process applications inside the Web server process (INETINFO.EXE) or as out-of-process applications in a separate MTS-managed process (MTX.EXE). The former offers somewhat better performance. The latter offers better fault isolation and debugging support. The INETINFO.EXE process runs as a Windows NT service under the Local System account. ASP scripts that execute in in-process ASP applications run as IUSR_MACHINENAME if authentication is not enabled. IIS-controlled MTX.EXE processes run as IWAM_MACHINENAME. ASP scripts in out-of-process ASP applications still run as the IUSR_MACHINENAME account if authentication is disabled. If authentication is enabled, ASP scripts always run with the Web client's credentials whether the ASP application is in-process or out-of-process.
Adding Server-side Objects
|
|
You can either put this into your Application_OnStart event handler or put it into a .VBS file and run it using Windows® Scripting Host.
With this metabase setting in place, you can now create out-of-process objects using either Server.CreateObject or the <object> tag. Since the object will live in a distinct process, the ASP client always gets back a proxy, and the process will run with the credentials indicated in the registry for the particular server. If the server is not configured to run as a particular user (that is, if it has no RunAs entry), it will start using the process credentials of the ASP application (either LocalSystem or IWAM_MACHINENAME). Note that there is a bug in Windows NT 4.0 Service Pack 3 or earlier that sometimes causes the wrong credentials to be used. You can install a post-SP3 hot fix to ensure that IIS 4.0 security works properly. So, implementing objects that have page scope is easy. Simply create ThreadingModel=Apartment objects and everything is fine. This is so simple that it can even be done in Visual Basic. But what if you want to write a component that will be used at session scope? This is a bit more complex. As mentioned earlier, the thread used to process ASP pages submitted by a particular session may change when there are no pending requests. This lack of thread affinity allows IIS to map requests to idle threads as efficiently as possible. If, however, you were to instantiate an STA-based object using <object> tags with session scope, that object's methods would always need to be serviced on the thread that processed the <object> tag. To avoid consuming two or more threads per page, ASP notices that you have bound an STA-based object to a session and ensures that all subsequent requests are serviced on the original thread. This forces the session to exhibit thread affinity (which is also true of all STA-based objects). A similar problem occurs when explicitly storing a reference to an STA-based object as a session object property. To avoid pinning a session to a particular thread, session-scope objects should be implemented as apartment-neutral (the COM term) or agile (the ASP term). Apartment-neutral objects can be accessed freely from any apartment in the process. Under Windows NT 4.0, objects indicate their apartment neutrality by aggregating the Freethreaded Marshaler (FTM). When ASP tries to store an object reference as a session property, it interrogates the object to verify that it is apartment-neutral as follows: |
|
This code fragment assumes that the variable CLSID_
FreeThreadedMarshaler has been initialized by calling GetUnmarshalClass on the FTM returned by CoCreateFreeThreadedMarshaler, shown in Figure 4. It would be poor style to simply hard code the FTM's CLSID, as it may change in future versions of the COM library. Also, simply aggregating the FTM will probably break your object. FTM-based objects must ensure that they are callable from any thread in the process. They shouldn't hold any apartment-relative object references and instead must use the Global Interface Table. (See my September 1997 column for more on apartment neutrality.)
Application-scoped Objects
Have a question about programming with ActiveX or COM? Send your questions via email to Don Box: dbox@develop.com or http://www.develop.com/dbox/default.asp. From the September 1998 issue of Microsoft Systems Journal. |