Wicked Code

Running ASMX Web Services on STA Threads

Jeff Prosise

Code download available at:WickedCode2006_10.exe(152 KB)

Contents

The Problem
The Solution
Conclusion

Many of the gnarliest issues you read about in Wicked Code come from real problems experienced by real people trying to build software that works. Recently, I came across a problem that I had never encountered before, but that I'm certain other developers have or someday will experience. The problem was one of concurrency, and had to do with ASMX Web services and legacy COM components. The development team who brought the issue to my attention had built an ASMX Web service that relied on legacy COM components written in Visual Basic® 6.0 to perform key processing tasks.

The design called for a high degree of parallelism in processing requests, so each request submitted to the Web service created its own Visual Basic component instance, and then called it. Performance was abysmal. Investigation proved that despite the number of Web service threads making concurrent calls to an equal number of Visual Basic component instances (even if there were 20 or more), only one component instance was executing at a time.

In effect, calls were being queued up at the doorway to COM and serialized into the respective component instances. Because some of the calls required 20 seconds or more to complete, the problem wasn't one the dev team could ignore. Instead, it was a showstopper—the product could not ship until the problem was rectified.

After countless hours of trying to get a handle on the problem, the dev team realized they were up against a wall. Something was causing all those concurrent calls to be executed sequentially rather than in parallel. But what? Was it COM? Was it Visual Basic 6.0? Or was it a nuance of the ASMX architecture? Whatever the problem was, a solution had to be found fast. Otherwise, the schedule would slip and the entire project would be placed in jeopardy.

The Problem

In a perfect world, everyone writing code for Microsoft platforms would write managed code and would never again have to deal with legacy technologies, such as COM. But the reality is that millions of lines of critical business logic are encapsulated in COM components—Visual Basic 6.0 COM components, in particular—and ASP.NET developers frequently have no choice but to call those components from managed code.

The Microsoft® .NET Framework does a fine job of allowing managed code to call out to unmanaged COM components. For example, the Framework's Tlbimp.exe utility can import COM type libraries and generate Runtime Callable Wrappers (RCWs) that permit unmanaged components to be called as if they were managed. But neither Tlbimp.exe, nor any other .NET tool, can mitigate the concurrency issues that arise when calls go out from managed code to unmanaged COM components.

It was a concurrency issue like this that produced the problem the dev team had encountered. Figure 1 shows the threading configuration of the Web service and the components that it has created. In this example, the Web service is processing five concurrent requests. ASMX Web services use the same HTTP pipeline that ASPX pages use, so each request has been allocated a thread by ASP.NET. ASP.NET threads are, in reality, COM multithreaded apartment (MTA) threads—that is, they're threads that run in a COM MTA. Because Visual Basic 6.0 COM components are incompatible with COM MTAs, the component instances that are created by the Web service threads are not in the MTA with their creator threads. Instead, they live in a single-threaded apartment, or STA, created by COM. The STA, like all COM STAs, is driven by a single thread. And because all five component instances share an STA, they share that single thread.

Figure 1 Default Apartment Configuration

Figure 1** Default Apartment Configuration **

The fact that all five component instances run on the same thread in the same apartment explains why calls are serialized. When one of the MTA threads calls a COM component, COM marshals the call from one apartment to the other, performing a thread switch in the process. If the STA thread is busy processing a call from one MTA thread, COM continues to queue calls from other MTA threads until the STA thread becomes available. All parallelism is lost when the call crosses the boundary from the MTA to the STA because the STA can only do one thing at a time. In effect, the STA is like a big mutual exclusion lock that causes caller B to wait until the call from caller A has completed.

You can prove that Web services run on MTA threads with the simple Web service, TestService.cs, shown in Figure 2. Its one and only Web method, Test, returns a string that indicates what type of COM apartment it's running in. Figure 3 shows what happens when you invoke the Test method from ASP.NET's autogenerated test harness. The string "MTA" in the results clearly shows that the request was processed by an MTA thread.

Figure 2 Testing for MTA Threads

using System; using System.Web; using System.Collections; using System.Web.Services; using System.Threading; [WebService(Namespace = "https://tempuri.org/")] [WebServiceBinding(ConformsTo = WsiProfiles.BasicProfile1_1)] public class TestService : System.Web.Services.WebService { [WebMethod] public string Test() { return Thread.CurrentThread.ApartmentState.ToString(); } }

Figure 3 Proof of MTA Threads

Figure 3** Proof of MTA Threads **(Click the image for a larger view)

The scenario depicted in Figure 1 may seem contrived, but in fact it's extraordinarily common. Because ASP.NET requests run on MTA threads by default, callouts to STA-based COM objects (this includes all Visual Basic 6.0 COM components) are marshaled from the ASP.NET MTA into the COM STA. Developers often don't realize that the STA is a choke point and that seemingly concurrent calls to individual component instances are actually serialized by COM. This fact only becomes obvious when individual calls take a long time to complete.

This would be less of a problem if COM would create multiple STAs—one per MTA thread that creates an object instance. But COM doesn't do that, in part because COM is loath to spin up new threads in a process. (COM+ will do that, but COM+ is a different entity from COM.) When an MTA thread creates an STA-based object, COM is forced to create a new thread to drive that STA. When a second MTA thread creates an STA-based object, COM places that object instance in the STA it created for the first object instance. The same goes for the third object instance, fourth object instance, and so on.

Fortunately, there is a convenient solution to the COM STA bottleneck that page developers can use. While including an AspCompat="true" attribute in an ASPX's Page directive provides COM components access to ASP-style infrastructure such as Request and Response objects, it also has the very desirable effect of creating an STA thread pool and processing requests for that page with STA threads. Besides eliminating the marshaling overhead incurred by MTA-to-STA thread switches, AspCompat="true" allows STA COM objects (specifically, instances of COM classes registered ThreadingModel="Apartment") to be created in their creators' apartments provided the creators are running in STAs themselves. If there are five request processing threads running in STAs and each creates an STA COM object, then all five object instances share an apartment—and a thread—with their creator. COM is happy to place each object instance in a separate STA because it doesn't have to create the STAs (or threads to drive them). Calls to the objects are no longer queued to a single thread, and the objects can execute code concurrently.

Alas, there is no equivalent of AspCompat="true" for ASMX files. The WebService directive won't accept an AspCompat attribute. It's left to the ASMX developer to come up with a way to put ASMX requests on STA threads to avoid the STA bottleneck. Now let's look at the means for doing this.

The Solution

Before coding, I scanned a number of forums and blogs. I found a post at www.epocalipse.com/blog/category/general that proposed a solution that was both elegant and concise. It outlined an idea to write an HTTP handler that derives from System.Web.UI.Page, register it as the handler for ASMX files, and use the AspCompat infrastructure already in the Page class to lend AspCompat support to ASMX. The post was even accompanied by sample code (which was correct in principle, albeit flawed in implementation).

I built on this to produce the Page derivative shown in Figure 4. When it invokes the handler, ASP.NET calls AspCompatWebServiceHandler's BeginProcessRequest method, since AspCompatWebServiceHandler implements the IHttpAsyncHandler interface. (Without that interface, ASP.NET would call the handler's synchronous ProcessRequest method instead.) BeginProcessRequest delegates to the AspCompatBeginProcessRequest method inherited from Page. This method serves as the gateway to the AspCompat infrastructure that is built into the Page class. Shortly after AspCompat processing begins, AspCompatWebServiceHandler's OnInit method is called. This creates a normal ASMX HTTP handler (new WebServiceHandlerFactory.GetHandler) and calls its ProcessRequest method, passing in the HTTP context for the current request. Thus, the request undergoes the normal ASMX processing, but it does so within an AspCompat wrapper that processes the request with an STA thread instead of an MTA thread.

Figure 4 Using STA within an AspCompat Wrapper

using System; using System.Web; using System.Web.UI; using System.Web.Services.Protocols; using System.Web.SessionState; public class AspCompatWebServiceHandler : System.Web.UI.Page, IHttpAsyncHandler, IRequiresSessionState { protected override void OnInit(EventArgs e) { IHttpHandler handler = new WebServiceHandlerFactory ().GetHandler( this.Context, this.Context.Request.HttpMethod, this.Context.Request.FilePath, this.Context.Request.PhysicalPath); handler.ProcessRequest(this.Context); this.Context.ApplicationInstance.CompleteRequest(); } public IAsyncResult BeginProcessRequest( HttpContext context, AsyncCallback cb, object extraData) { return this.AspCompatBeginProcessRequest( context, cb, extraData); } public void EndProcessRequest(IAsyncResult result) { this.AspCompatEndProcessRequest(result); } }

In order for AspCompatWebServiceHandler to work its magic, it must be registered as the HTTP handler for ASMX files. You can register it using this web.config file:

<configuration> <system.web> <httpHandlers> <add verb="*" path="*.asmx" type="AspCompatWebServiceHandler, __code" /> </httpHandlers> </system.web> </configuration>

Note the assembly name used in the type attribute: __code. This assumes that you have placed the source code for AspCompatWebServiceHandler in the application's App_Code directory and allowed ASP.NET to generate an assembly from it. If, instead, you compiled the assembly yourself and place it in the bin directory, simply replace __code with the assembly name. (Since ASP.NET 1.x doesn't support App_Code directories, you'll have to do this if you're not running ASP.NET 2.0.)

In order to confirm that ASMX requests are now processed with STA threads, run TestService again after installing AspCompatWebServiceHandler. Figure 5 shows the results. The thread that ASP.NET assigned to the request is clearly an STA thread. Mission accomplished!

Figure 5 Confirming Use of STA Threads

Figure 5** Confirming Use of STA Threads **(Click the image for a larger view)

While it is a bit of a hack (there's often a very fine line between a hack and wicked code!), this solution worked flawlessly on the problem application. Figure 6 shows the threading and apartment configuration of the modified app. The five request processing threads are now STA threads living in separate STAs. Because COM places STA object instances in the STAs of their creators when the creators are STA threads, each object instance now has its own STA. Moreover, calls to the objects now execute in parallel because each object instance runs on a separate thread.

Figure 6 New Apartment Configuration

Figure 6** New Apartment Configuration **

Conclusion

Besides eliminating MTA-to-STA marshaling overhead, including the AspCompat="true" attribute allows COM to place object instances in separate STAs rather than lumping them into one apartment (and running them on one thread). Although AspCompat isn't supported for Web services, you can add it to your ASMX code and enjoy the same efficiency in calling Visual Basic 6.0 COM components from Web services as you do from Web pages.

If you'd like to learn more about COM threading and apartments, check out a pair of articles I wrote back when COM was cool. The first is available at www.codeguru.com/cpp/com-tech/activex/apts/article.php/c5529 and the second at www.codeguru.com/cpp/com-tech/activex/apts/article.php/c5533. The articles are several years old, but the content is just as pertinent today as it was back then—especially if you're a programmer who's into .NET and faced with the challenge of calling old COM components from managed code.

Send your questions and comments for Jeff to  wicked@microsoft.com.

Jeff Prosise is a contributing editor to MSDN Magazine and the author of several books, including Programming Microsoft .NET (Microsoft Press, 2002). He is also a cofounder of Wintellect, a software consulting and education firm.