More on COM Objects and Threads in .NET

If you need to do COM with legacy objects that are not thread safe and you want to access COM objects from multiple threads, you can't use the ThreadPool


June 07, 2005
URL:http://www.drdobbs.com/mobile/mobile/more-on-com-objects-and-threads-in-net/184406096


[Part 2 of 2] In the previous column, I left you in limbo with why the [STAThread] attribute on your “main” method (or whatever entry point method you use) isn’t enough to guarantee COM Interop will do the right thing if you’re multithreading. Now I’ll wrap up the mystery.

The [STAThread] attribute is really cool and is a prime example of why, at times, COM Interop can seem like magic. This single attribute tells .NET that if the application interacts with COM objects, it should do so as STA’s (single threaded apartment, a.k.a. apartment model). This is the most common way to interact with COM for the majority of developers; in particular, any legacy OLE objects required STA. To be really helpful, the Visual Studio .NET App Wizard will automatically add this attribute to any applications it generates. If you remove the attribute, COM Interop still works, but it reverts to MTA model (multithreaded apartment). You could also explicitly set the model to MTA by changing the [STAThread] attribute to [MTAThread]. But most developers interacting with COM wouldn’t bother. And it’s this default behavior of .NET to favor MTAs rather than STAs that will trip you up.

Recall that the most straightforward (and recommended) way for .NET developers to create secondary (worker) threads in their application is to use ThreadPool.QueueUserWorkItem. This dandy little method makes it a snap to spin off a thread efficiently—when a thread “terminates” or completes its operation, the ThreadPool adds it back to its container of available threads rather than actually destroying it. This is much more efficient than using the Thread class directly and makes sense most of the time—an exception is when things like thread prioritization need to be specified.

So to use ThreadPool.QueueUserWorkItem, you’d write some code that looks like this:

public void DoStuff()
{
ThreadPool.QueueUserWorkItem( new WaitCallback(WorkItem),1 );

//.. do more stuff in primary thread.
}

In the call to QueueUserWorkItem, I create an instance of the WaitCallback class that wraps an ordinary method in my class named WorkItem. This method is where I begin the work for my secondary thread. I'm passing it a parameter with the value of "1. Actually, QueueUserWorkItem is flexible—you can pass any .NET object you wish to the method and that object will be dutifully passed to the method you passed into WaitCallback. Here is how that code looks:

private void WorkItem(object o)
{
	try
	{
		SimpleSTAClass c = 
			new SimpleSTAClass();
		c.HelloWorld();
	}
	catch(Exception e )
	{
		System.Windows.Forms.MessageBox.Show( e.Message );
	}
}

The method takes a single parameter of type object, which is really the parameter I passed into QueueUserWorkItem earlier. The body of the WorkItem method could be elaborate if need be, but in my case, I simply create an instance of an STA object that I added as a reference to my project:

All is well, yes? Actually, no. This is where you can get bitten. This code will compile and run just fine; however, as my code gets more complex, it starts acting strangely as more threads interact with this legacy COM object. But the code is set to use STAThread, so how can this be?

Looking more closely at the QueueUserWorkItem method documentation, there’s no mention of anything about COM. Hmm. Moving up the chain of documentation to a more general description of thread pools, we come across this gem:

There are several scenarios in which it is appropriate to create and manage your own threads instead of using the ThreadPool. You should do so:

The third item is the culprit. All threads created in the ThreadPool are MTA threads, not STA threads. Yikes. If you recall from COM, all threads must initialize COM by calling CoInitialize or CoInitializeEx explicitly. Not doing so meant COM basically didn't work in that thread. In .NET, the CLR and COM Interop handle this for you invisibly. Trying to be helpful, .NET says we'll use our default COM model of MTA (as I mentioned earlier, it does if you leave off the [STAThread] attribute from the entry point method of your application). So you now have a case where your application's main thread is using STA and the worker thread is using MTA. Very ugly if the COM object states it's not thread safe.

You might be thinking you could just override the ApartmentState property of the Thread object created from the ThreadPool. Besides being a bad idea, it doesn’t work. Doing something like this as the first line of code in method WorkItem:

Thread.CurrentThread.ApartmentState = ApartmentState.STA;

results in basically nothing—the thread simply ignores the attempt to override the apartment state. A good thing, because these threads are in a thread pool and forgetting to reset the state back to MTA could really mess up other attempts to use the thread pool where MTA threads were expected.

So, the net of all this is that if you need to do COM with legacy objects that are not thread safe and you want to access COM objects from multiple threads, you can’t use the ThreadPool. I suggest reading the MSDN Library paragraph describing the situations where you shouldn’t use a ThreadPool carefully. If you do a lot of work with Threads, print it out and paste it on your wall. You’ll be glad you did when working with .NET and COM.


Mark M. Baker is the Chief of Research & Development at BNA Software located in Washington, D.C.
Do you have a Windows development question? Send it to [email protected].


Terms of Service | Privacy Statement | Copyright © 2024 UBM Tech, All rights reserved.