|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Announcements
Chapters
Services
Feature Zones
|
IntroductionWith 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 A little background on threading in COMThreading 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 [ Microsoft's solution to this problem is to run the page itself in STA. This is accomplished with the 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
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. 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 PooledASPCompatRunner
The solution using 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 A note on the design and techniques
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
Nested Classes
Working with nested classes is easier in Java than in .NET. In Java the " 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 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
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 completionBy defaultPooledASPCompatRunner 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();
OutputExec.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
References
|
|||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||