Larry Osterman's WebLog

Confessions of an Old Fogey
Blog - Title

What are these "Threading Models" and why do I care?

What are these "Threading Models" and why do I care?

  • Comments 28

Somehow it seems like it’s been “Threading Models” week, another example of “Blogger synergy”.  I wrote this up for internal distribution to my group about a year ago, and I’ve been waiting for a good time to post it.  Since we just hit another instance of the problem in my group yesterday, it seemed like a good time.

 

So what is this thing called a threading model anyway?

Ok.  So the COM guys had this problem.  NT supports multiple threads, but most developers, especially the VB developers at which COM/ActiveX were targeted are totally terrified by the concept of threading.  In fact, it’s very difficult to make thread-safe VB (or JS) applications, since those languages don’t support any kind of threading concepts.  So the COM guys needed to design an architecture that would allow for supporting these single-threaded objects and host them in a multi-threaded application.

The solution they came up was the concept of apartments.  Essentially each application that hosts COM objects holds one or more apartments.  There are two types of apartments, Single Threaded Apartments (STAs) and Multi Threaded Apartments (MTAs).  Within a given process there can be multiple STA’s but there is only one MTA.

When a thread calls CoInitializeEx (or CoInitialize), the thread tells COM which of the two apartment types it’s prepared to host.  To indicate that the thread should live in the MTA, you pass the COINIT_MULTITHREADED flag to CoInitializeEx.  To indicate that the thread should host an STA, either call CoInitialize or pass the COINIT_APARTMENTTHREADED flag to CoInitializeEx.

A COM object’s lifetime is limited to the lifetime of the apartment that creates the object.  So if you create an object in an STA, then destroy the apartment (by calling CoUninitialize), all objects created in this apartment will be destroyed.

Single Threaded Apartment Model Threads

When a thread indicates that it’s going to be in single threaded apartment, then the thread indicates to COM that it will host single threaded COM objects.  Part of the contract of being an STA is that the STA thread cannot block without running a windows message pump (at a minimum, if they block they must call MsgWaitForSingleObject – internally, COM uses windows messages to do inter-thread marshalling).

The reason for this requirement is that COM guarantees that objects will be executed on the thread in which they were created regardless of the thread in which they’re called (thus the objects don’t have to worry about multi-threading issues, since they can only ever be called from a single thread).  Eric mentions “rental threaded objects”, but I’m not aware of any explicit support in COM for this.

 

Multi Threaded Apartment Model Threads

Threads in the multi threaded apartment don’t have any restrictions – they can block using whatever mechanism they want.  If COM needs to execute a method on an object and no thread is blocked, then COM will simply spin up a new thread to execute the code (this is particularly important for out-of-proc server objects – COM will simply create new RPC threads to service the object as more clients call into the server).

How do COM objects indicate which thread they work with?

When an in-proc COM object is registered with OLE, the COM object creates the following registry key:

            HKCR\CLSID\{<Object class ID>}\InprocServer32

The InprocServer32 tells COM which DLL hosts the object (in the default value for the key), and via the ThreadingModel value tells COM the threading model for the COM object.

 

There are essentially four legal values for the ThreadingModel value.  They are:

Apartment

Free

Both

Neutral

Apartment Model objects.

When a COM object is marked as being an “Apartment” threading model object, it means that the object will only run in an STA thread.  All calls into the object will be serialized by the apartment model, and thus it will not have to worry about synchronization.

Free Model objects.

When a COM object is marked as being a “Free” threading model object, it means that the object will run in the MTA.  There is no synchronization of the object.  When a thread in an STA wants to call into a free model object, then the STA will marshal the parameters from the STA into the MTA to perform the call. 

Both Model objects.

The “Both” threading model is an attempt at providing the best of both worlds.  An object that is marked with a threading model of “Both” takes on the threading model of the thread that created the object. 

Neutral Model objects.

With COM+, COM introduced the concept of a “Neutral” threading model.  A “Neutral” threading model object is one that totally ignores the threading model of its caller.

COM objects declared as out-of-proc (with a LocalServer32=xxx key in the class ID.) are automatically considered to be in the multi-threaded apartment (more about that below).

It turns out that COM’s enforcement of the threading model is not consistent.  In particular, when a thread that’s located in an STA calls into an object that was created in the MTA, COM does not enforce the requirement that the parameters be marshaled through a proxy object.   This can be a big deal, because it means that the author of COM objects can be lazy and ignore the threading rules – it’s possible to create a COM object in that uses the “Both” threading model and, as long as the object is in-proc, there’s nothing that’ll check to ensure you didn’t violate the threading model.  However the instant you interact with an out-of-proc object (or call into a COM method that enforces apartment model checking), you’ll get the dreaded RPC_E_WRONG_THREAD error return.  The table here describes this in some detail.

What about Proxy/Stub objects?

Proxy/Stub objects are objects that are created by COM to handle automatically marshaling the parameters of the various COM methods to other apartments/processes.  The normal mechanism for registering Proxy/Stub objects is to let COM handle the registration by letting MIDL generate a dlldata.c file that is referenced during the proxy DLL’s initialization.

When COM registers these proxy/stub objects, it registers the proxy/stub objects with a threading model of “Both”.  This threading model is hard-coded and cannot be changed by the application.

What limitations are there that I need to worry about?

The problem that we most often see occurs because of the Proxy/Stub objects.  Since the proxy/stub objects are registered with a threading model of “Both”, they take on the threading model of the thread that created the object.  So if a proxy/stub object is created in a single threaded apartment, it can only be executed in the apartment that created it.  The proxy/stub marshaling routines DO enforce the threading restriction I mentioned above, so applications learn about this when they unexpectedly get a RPC_E_WRONG_THREAD error return from one of their calls.  On the server side, the threading model of the object is set by the threading model of the caller of CoRegisterClassObject.  The good news is that the default ALT 7.1 behavior is to specify multi-threaded initialization unless otherwise specified (in other words, the ATL header files define _ATL_FREE_THREADED by default.

How do I work around these limitations?

Fortunately, this problem is a common problem, and to solve it COM provides a facility called the “Global Interface Table”.  The GIT is basically a singleton object that allows you to register an object with the GIT and it will then return an object that can be used to perform the call from the current thread.  This object will either be the original object (if you’re in the apartment that created the object) or it will be a proxy object that simply marshals the calls into the thread that created the object.

If you have a COM proxy/stub object (or you use COM proxy/stub objects in your code), you need to be aware of when you’ll need to use the GIT to hold your object.

Use the GIT, after you’ve called CoCreateInstance to create your COM object, call IGlobalInterfaceTable::RegisterInterfaceInGlobal to add the object to the global interface table.  This will return a “cookie” to you.  When you want to access the COM object, you first call IGlobalInterfaceTable::GetInterfaceFromGlobal to retrieve the interface.  When you’re done with the object, you call IGlobalInterface::RevokeInterfaceFromGlobal.

In our case, we didn’t feel that pushing the implementation details of interacting with the global interface table to the user was acceptable, so we actually wrote an in-proc object that wraps our out-of-proc object. 

Are there other problems I need to worry about?

Unfortunately, yes.  Since the lifetime of a COM object is scoped to the lifetime of the apartment that created the object, this means that when the apartment goes away, the object will go away.  This will happen even if the object is referenced from another thread.  If the object in question is a local object, this really isn’t that big a deal since the memory backing the object won’t go away.  If, however the object is a proxy/stub object, then the object will be torn down post-haste.  The global interface table will not help this problem, since it will remove all the entries in the table that were created in the apartment that’s going away.

Additional resources:

The MSDN article Geek Speak Decoded #7 (http://msdn.microsoft.com/library/default.asp?url=/library/en-us/dngeek/html/geekthread.asp) also has some detail on how this stuff works (although it’s somewhat out-of-date).

 

  • There's also the "Main" threading model which everyone should avoid. It exists for backwards compatibility with people who don't know how threads work.
  • And here's what happens in .NET when you totally forget to specify a threading model at all:

    http://staff.develop.com/candera/weblog2/PermaLink.aspx?guid=4c8cde3c-5c62-43f3-bfe0-61feceab8032

    :p
  • To be technical, in this case you forgot to call CoInitializeEx(NULL, COINIT_APARTMENTTHREADED). The SHBrowseFolder API (I think that's the one) is documented as requiring that it run in an STA. If you don't have the [STAThread] attribute on your thread process, the CLR initializes your thread in the MTA.
  • Found it - From the MSDN documentation on SHBrowseForFolder:

    You must initialize Component Object Model (COM) using CoInitializeEx with the COINIT_APARTMENTTHREADED flag set in the dwCoInit parameter prior to calling SHBrowseForFolder. You can also use CoInitialize or OleInitialize, which always use apartment threading.

  • At least one site (http://theserverside.net/news/thread.tss?thread_id=25595) picked up on this post yesterday, which is cool (I love readers :). But I'll be honest and say that I think their post's a smidge off topic.

    My article was intended to be a commentary about what happens from the side of a component author - which threading model should you chose and why, and not really as guidelines for when (or why) you should use the [STAThread] or [MTAThread] attributes on your managed code. But clearly the [STAThread]/[MTAThread] info can be inferred from the text.

    By default, all CLR threads run in the MTA, you have to explicitly tag them as running in the STA. Which means that if (like Craig) you're interacting with a component that can only work in an STA, you can get burned pretty badly. And the worst thing is that it's not always obvious what the threading model of the component is, it's not always clearly documented, sometimes you need to dig into the registry.
  • Agreed - while I've long since internalized the rules around threads apartments, the problem is that it's not immediately obvious when you're violating them.

    In my case, the most heinous thing was the fact that it *sort of* worked without [STAThread]. That made the failure mode very unfamiliar and hard to figure out.

    Sorry if I took your post off-track. It just reminded me of the most mysterious manifestation of apartment confusion I've encountered recently.
  • It didn't. And I have no idea why on earth SHBrowseForFolder shouldn't work in the MTA - the apartment model restrictions are typically to protect components from being accessed from multiple threads simultaneously, which I wouldn't think happened in this case.
  • It's likely for one of two reasons:

    1) The SHBrowseForFolder stuff communicates asynchronously with a background thread via a message pump, assuming the thread affinity of the component will handle synchronization properly.
    2) The component creates some other component in an STA and the resulting inter-apartment marshalling introduces the bug.

    I'm totally guessing, of course.
  • Raymond makes other people discuss stuff so he doesn't have to.
  • Maybe someone can help my understanding with regard to firing events between threads.

    One scenario that I have often had is as follows:

    I have an ATL COM component doing some work and a user interface in VB6. In the ATL component, I start a worker thread and want to fire events which will be handled by the user interface component in VB6.

    In practice, I know that firing events from the worker thread leads to problems, so what I do is call an intermediate method within my object, which then fires the event. The intermediate method runs in the user interface thread and not in the worker thread.

    The question is, why do I need to do this? Why can't I fire an event from a worker thread which then runs in the user interface thread.

    Surely marshalling works on events in just the same manner as it works on other COM calls.

    Phil
  • As I understood it, the main point of the STA was to integrate the threading of COM components, especially GUI-oriented ActiveX controls, with the single-threaded message pump used for windows. Even in .NET this problem still lingers and you have to call Control.Invoke to effect a thread switch or risk erratic consequences. It's not about synchronizing access to a single object (COM+ has a separate option for that), but about synchronizing access to the entire apartment and all objects within. SHBrowseForFolder probably runs into problems only because of the windowing it tries to use.
Page 1 of 2 (28 items) 12