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.
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 {
AutoResetEvent wait = this[d];
if (wait != null) wait.Set();
}
OperationCompletionDelegate cb =
_callbacks.ContainsKey(d) ? _callbacks[d] : null;
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.
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;
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 {
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;
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);
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);
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
- MSDN - @ Page
- MSDN - Wicked Code
- Code Project - Understanding The COM Single-Threaded Apartment Part 1