Click here to Skip to main content
Click here to Skip to main content

A threading framework to optimize interoperation between .NET and apartment threaded COM components

, 7 May 2007
Rate this:
Please Sign up or sign in to vote.
A generic solution and an accompanying threading framework to optimize calls between .NET and apartment threaded COM components

Introduction

With the explosion of technologies in the .NET stream many developers may have forgotten about COM. However, due to some recent encounters it has remained in generation 2 of garbage collection in my brain!

While there are many articles on COM interop, this article concentrates on threading issues associated with using apartment threaded components in .NET hosts. I also propose a generic solution and an accompanying threading framework that reduces thread switching that occurs in chatty calls between .NET and apartment threaded COM components.

The first half of this article is a little fixated on COM. It's not all legacy talk, however. In the latter half I touch upon some design elements of the framework that make use of some of the newer .NET 2.0 concepts such as partial and nested classes, parameterized thread start delegates and generics apart from legacy threading using COM+ thread pooling. The accompanying demo (test) project consists of polymorphic unit tests using NUnit and also NUnitASP where I use reflection and stack trace to test private methods and state.

A little background on threading in COM

Threading is an area of concern when using VB6 COM components in .NET. VB6 components use a threading model known as STA (Single Threaded Apartment). The STA has a message queue associated with a hidden window and a single thread that processes messages off the queue. Method invocations on the STA component are serialized into the queue similar to the working of queued components in COM+. The STA may contain multiple instances of a component but method invocations on instances are processed sequentially by the lone STA thread in the apartment. This concept of call serialization avoids race conditions between multiple method invocations on the instance.

The problem

An apartment threaded component instance is created in the client's apartment if the threading models are compatible. If not, the instance is hosted in a new STA. NET threads run in MTA (Multi Threaded Apartment) by default unless the entry point is marked with the [STAThreadAttribute]. A method call from ASP.NET to an apartment threaded component leads to the creation of an STA to host the component instance. Calls to the object involve a thread switch from the client's MTA thread to the component's STA thread due to incompatibility of the threading models, which affects performance.

Microsoft's solution to this problem is to run the page itself in STA. This is accomplished with the ASPCompat 1 attribute in the page directive. The web service directive on the other hand does not support this. Jeff Prosise published an approach 2 in the October 2006 edition of Wicked Code where he creates a custom ASMX handler that derives from the Page handler and delegates ASMX request processing to the web service handler that runs in STA via the call to the Page handler's AspCompatBeginProcessRequest method.

There are a few hidden drawbacks of running the entire page/web service life cycle in STA. The way it works is the page instance is initially created by the MTA thread drawn from the managed thread pool. When the ASPCompat attribute on a page is set to true, the request and rest of the page lifecycle is handed over to an STA thread by the ASP.NET infrastructure. This can be verified by recording the apartment states of the current thread in the web page's constructor (MTA) and in the page load method (STA) when the ASPCompat attribute is set to true. ASP.NET I/O and worker threads that are configurable via process model in machine.config come from MTA thread pool threads. Also, from stress tests it is noted that there is significant underperformance associated with running a page in STA. The solution I propose removes any binding to the hosting environment. It can be used in multithreaded WinForms, web page and web service applications without constraining the hosting environment to run in STA.

Screenshot - ASPCompat1.jpg

The idea

With N tier systems the context of legacy COM interoperation could be anywhere between the layers and the host could be a web application, a web service or other application types. Once the apartment state of a thread is set, changing it later does not have any effect. This has to be done before an entry point is executed by the thread. ASPCompatRunner spawns a foreground thread that has been initialized to STA. .NET code that involves interoperation with apartment COM components is wrapped into a method. The idea is to execute the .NET method itself in this STA thread so that any COM component that is instantiated in the method lives in the same apartment as the .NET code. This avoids thread switching between STA and MTA that would come about when the .NET Com Callable Wrapper (that runs in MTA) invokes methods on the unmanaged STA COM component.

One or more methods that involve COM interop calls are registered with the task scheduling framework and the registered set is executed as a batch in STA.Two types of .NET methods can be registered with the framework – void methods and methods that take a single object parameter. These methods are wrapped respectively in ThreadStart and the new ParameterizedThreadStart delegates defined in the System.Threading namespace. A callback can be optionally registered that corresponds to a method. The framework has provisions for invoking callbacks that are called on completion of the corresponding scheduled task. Callback invocation can either be synchronous on the same STA thread or asynchronous on a background MTA thread, depending on the value of the ExecAsyncCallback property. The callback marshals back metadata about the state, method that was invoked and possible exception back to the caller. Another feature implemented by the framework is the ability for the client to wait on a specific registered task or to wait for all registered tasks to complete.

PooledASPCompatRunner

The solution using ASPCompatRunner is not ideal as it involves spawning a thread for each instance, although the thread can execute one or more registered methods in STA. Spawning new threads will strain server resources and also reduce performance due to excessive context switching between the worker threads. PooledASPCompatRunner avoids this overhead by plugging into the STA thread pool provided by COM+ instead of using its own threads. In most cases it is advisable to use the PooledASPCompatRunner class for running .NET code blocks (tasks) in STA.

The esoteric COM+ thread pool

Many .NET developers may be aware of COM+ freebies such as object pooling, contexts, transaction support, etc. A little less known feature is the COM+ support for STA thread pooling. The System.EnterpriseServices namespace contains the Activity class and IServiceCall interface that provide the hooks into COM+ thread pools. When InvokeAsynchronously property is true, PooledASPCompatRunner allows the client to run the registered tasks asynchronously and get notified through callbacks on completion.

A note on the design and techniques

ASPCompatRunner uses generic datastructures to store delegates, method parameters, callbacks and thread synchronization objects. One of the reasons that I use generics instead of multicast delegates is because there are two different types of operations - ThreadStart and ParameterizedThreadStart delegates. The Delegate.Combine method will only merge delegates of the same type. I won't go into the details here. The meat is in the code! I would suggest browsing through the ASPCompat and NUnitTests projects included in the solution for a more detailed understanding. The class diagram above along with Using the code section should be helpful for an overview. InvokeDelegates is a virtual method whose base implementation in ASPCompatRunner runs in a foreground STA thread. It executes registered tasks on this STA thread.

protected virtual void InvokeDelegates()
{
    foreach (DictionaryEntry operation in _operations) 
    {
        object d = operation.Key;
        Exception ex;
        try {
            if (d is ParameterizedThreadStart)
                (d as ParameterizedThreadStart)(operation.Value);
            else (d as ThreadStart)();
            ex = null;
        }
        catch (Exception e) {
            ex = e;
        }
        finally {
            // Indexer over an operation returns the corresponding thread 
            // synchronization event
            AutoResetEvent wait = this[d];
            if (wait != null) wait.Set();
        }
        OperationCompletionDelegate cb =
            _callbacks.ContainsKey(d) ? _callbacks[d] : null;
        /* If a callback was registered invoke it
         * The default behaviour is to invoke a callback on the same STA 
         * thread
         * If ExecAsyncCallback = true, callbacks are invoked asynchronously 
         * on a threadpool MTA thread
         */
        Delegate p = d as Delegate;
        if (cb != null)
        {
            OperationCompletionMetadata data = new OperationCompletionMetadata(
                p.Target, p.Method, ex);
            if (_execAsyncCallback)
                cb.BeginInvoke(data, null, null);
            else cb.Invoke(data);
        }
    }
}

The following sequence diagram illustrates the difference between PooledASPCompatRunner and ASPCompatRunner due to polymorphic overrides.

Screenshot - ASPCompat2.jpg

Nested Classes

Working with nested classes is easier in Java than in .NET. In Java the "this" keyword can be used to access instance members of an inner class as well as the containing outer class. The compiler implicitly creates an outer class instance that is bound to the "this" identifier of the inner class instance. In C# there is no binding between the outer and inner classes. Hence I use the .NET iterator pattern to pass an outer class reference to the inner class as the following code snippet shows.

private class Task : IServiceCall
{
    private PooledASPCompatRunner _container;            

    // Can be a ThreadStart or ParameterizedThreadStart delegate
    object _operation; 
    

    private Task() { }

    public Task(PooledASPCompatRunner container, object operation) 
    {
        _container = container; _operation = operation;                
    }
    //...
}

In the case of PooledASPCompatRunner, a COM+ activity executes a task. A task is associated with an operation delegate but the delegate collection is held in the base ASPCompatRunner instance. A nested class structure enables the inner class (Task) to access the private operation collection without any change in accessibility.

public void OnCall()
{
    Exception ex = null;
    try {
        if (_operation is ParameterizedThreadStart) {
            object arg = _container._operations[_operation];
            (_operation as ParameterizedThreadStart)(arg);
        }
        else (_operation as ThreadStart)();
    }
    catch (Exception e) {
        ex = e;
    }
    finally {
        //  Signal to the next waiting thread
        AutoResetEvent hnd = null;
        if (_container._locks.ContainsKey(_operation))
            hnd = _container._locks[_operation];
        if (hnd != null) hnd.Set();
    }
    OperationCompletionDelegate cb =
        _container._callbacks.ContainsKey(_operation)
            ? _container._callbacks[_operation] : null;
    /* If a callback was registered invoke it
     * The default behaviour is to invoke a callback on the same STA thread
     * If ExecAsyncCallback = true, callbacks are invoked asynchronously on a 
     * threadpool MTA thread
     */
    Delegate p = _operation as Delegate;
    if (cb != null) {
        OperationCompletionMetadata data = 
            new OperationCompletionMetadata(p.Target, p.Method, ex);
        if (_container._execAsyncCallback)
            cb.BeginInvoke(data, null, null);
        else cb.Invoke(data);
    }
}

Using the code

ASPCompatRunner and PooledASPCompatRunner expect ThreadStart and ParameterizedThreadStart delegate parameters. With .NET 2.0 delegate inferencing wiring delegates can be done elegantly without the need to explicitly wrap methods with these delegates.

Simplistic case (invoking a void method)

PooledASPCompatRunner p = new PooledASPCompatRunner();
p.Execute(obj.Method); // Method is declared as void Method();
// do some other work...
p.WaitOnAllOperations();

Invoking a method with arguments and callback on completion

By default PooledASPCompatRunner invokes operations asynchronously unless the property InvokeAsynchronously is set to true. OnOperationCompleted is a callback function which can be directly referenced instead of the following .NET 1.x declaration
ASPCompatRunner.OperationCompletionDelegate callBack = new ASPCompatRunner.OperationCompletionDelegate(OnOperationCompleted);
PooledASPCompatRunner p = new PooledASPCompatRunner();
ParameterizedThreadStart operation = obj.MethodWithArg;
p.Execute(operation,
                "method parameter",
                OnOperationCompleted);
// do some other work...
// wait for that operation to complete
p.WaitOnOperation(operation);

Invoking a set of methods in one go (multiple tasks per execute call)

One can schedule a batch of operations as follows. Callbacks (OnOperationCompleted) are executed on the background STA thread unless the property ExecAsyncCallback is set to true. The code snippet below uses a call to WaitOnAllOperations to block the main thread till all operations have completed. This may not be necessary in all cases, especially if some of the operations in the registered batch are fire and forget type. In that case one should create explicit references to the delegates and call WaitOnOperation(operation) to block the main thread on a specific operation.
PooledASPCompatRunner p = new PooledASPCompatRunner();
p.Register(obj1.MethodWithArg, "arg1", OnOperationCompleted);
p.Register(obj2.MethodWithArg, "arg2", OnOperationCompleted);
p.Register(obj3.MethodWithArg, "arg3", OnOperationCompleted);
p.Register(obj4.Method, OnOperationCompleted);
p.Execute();
p.WaitOnAllOperations();

Output

Exec.MethodWithArg was called with parameter [arg3]
                from Thread 12 in state STA 
Exec.MethodWithArg was called with parameter [arg2]
                from Thread 14 in state STA 
Exec.Method was called with parameter []
                from Thread 15 in state STA 
Exec.MethodWithArg was called with parameter [arg1]
                from Thread 13 in state STA 
OnOperationCompleted() thread apt = STA, 
    id = 12 on NUnitTests.Exec.MethodWithArg 
OnOperationCompleted() thread apt = STA, 
    id = 14 on NUnitTests.Exec.MethodWithArg 
OnOperationCompleted() thread apt = STA, 
    id = 15 on NUnitTests.Exec.Method 
OnOperationCompleted() thread apt = STA, 
    id = 13 on NUnitTests.Exec.MethodWithArg 
Main thread apartment = MTA id = 11

Conclusion

I was curious to know how executing a batch of tasks in one call compares with executing one task per call. The WebTest project contains some superficial performance tests using NUnitASP or System.Net.WebClient. Also a web stress test was done using the free Web Application Stress Tool (WAST) for a duration of 5 minutes to give a rough indication on comparative performance. The disclaimer is that these tests are superficial and do not cover a wide range of load conditions. I have used PooledASPCompatRunner and not the base ASPCompatRunner, as the former is assumed to be a better alternative for typical loads associated with the web environment.

PooledASPCompatRunner running multiple tasks per Execute call appears to give better performance than calling Execute each time for a task. COM interop using PooledASPCompatRunner is almost 10 times faster than regular COM interop. Although using ASPCompat attribute in the Page directive is comparable to using PooledASPCompatRunner the problem with the former is that it constrains the entire page and subsequent methods in the call stack to run in STA. Using PooledASPCompatRunner is advantageous in an N Tier system that contains a COM component in one of the layers as it doesn't constrain the entire flow across the layers to execute in STA. Instead a particular method in a layer can separately execute in STA while the rest of the flow can execute in managed MTA threads over which we have fine grained control through configuration entries in machine.config.

RPS TTFB TTLB Time (NUnitASP/WebClient)
Regular COM interop 35.58 279.98 280 31.84
Page directive with ASPCompat 490.99 19.26 19.29 6.27
PooledASPCompatRunner
Multiple tasks per execute call
840.29 10.79 10.82 3.21
PooledASPCompatRunner
One task per execute call
730.17 12.58 12.61 7.97
* time in seconds

References

  1. MSDN - @ Page
  2. MSDN - Wicked Code
  3. Code Project - Understanding The COM Single-Threaded Apartment Part 1

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here

Share

About the Author

Rohit Gadagkar
Web Developer
United States United States
I am a tech lead working for Cap Gemini. Although I primarily work with the Microsoft technology stack (including .NET and legacy technologies) I like to keep myself informed about developments in the Java world.

Comments and Discussions

 
GeneralReturn value PinmemberCrazyDeveloper_217-May-11 6:13 
GeneralUsing this library in Windows Applications PinmemberMursil21-Jul-07 1:10 
GeneralRe: Using this library in Windows Applications PinmemberRohit Gadagkar26-Aug-07 5:23 
Generalthis keyword PinmemberSteve Hansen8-May-07 2:35 
That's what you get for using this when it isn't needed:
 
class A
{
string field = "My String";
 
class B
{
public void PrintField(A a)
{
// nested class can access private fields of A
System.Console.WriteLine(a.field); // this points to B, so this.field would throw compile error
}
}
}

GeneralRe: this keyword PinmemberRohit Gadagkar8-May-07 11:32 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.

| Advertise | Privacy | Mobile
Web03 | 2.8.140814.1 | Last Updated 7 May 2007
Article Copyright 2007 by Rohit Gadagkar
Everything else Copyright © CodeProject, 1999-2014
Terms of Service
Layout: fixed | fluid