When you create your class factory in a multi-threaded apartment, it can be called from multiple threads. Hence the name "multi-threaded". Why exactly do you find it surprising?
Specifically, COM runtime maintains a pool of threads that execute cross-apartment calls into MTA. Any object that declares itself to be multi-threaded can then be called on any of those threads.
and then if i spawned a third thread that was also in a STA, creating the object there would put it in the first com-spawned MTA thread, not a new one
This statement doesn't make much sense. Multi-threaded objects don't belong to any particular thread, so it's not clear what you mean by "object ... put ... in MTA thread". A multi-threaded object may be created, and called on, any thread that joined MTA (whether a thread your program created explicitly, or one created by COM runtime); it may be called by several such threads simultaneously.
The difference in behavior you observe is due to this fact. Cross-apartment calls are delivered to STA threads in the form of window messages. An STA thread signals its readiness to accept incoming calls by calling GetMessage
. On the other hand, cross-apartment calls into MTA do not use window messages, but some other, undocumented and unspecified, mechanism. Such a call can only be served by a thread from the COM-created thread pool - COM runtime cannot just commandeer a thread you've explicitly created, since it doesn't know what that thread is doing at any given time. There is no API that allows your thread to say "I'm ready to accept and execute arbitrary COM calls" - to join COM's thread pool, in effect.
With this in mind, let's look at your scenarios. Case A: you have a regular COM object registered with ThreadingModel=Free
, no funny business with custom class factory. An STA thread calls CoCreateInstance
with that object's CLSID
. COM reads the information from the registry, discovers that the object is multi-threaded, and marshals the call to one of the threads in its MTA thread pool, which creates the object and marshals its interface pointer back. If an STA thread (either the same one, or another one) calls CoCreateInstance
again with the same CLSID
, the process is repeated, and it may just so happen that the same thread from the pool handles it.
By the way, the thread that created the object doesn't have to be the same thread that handles OutputOwningThreadId
call. In fact, if you call OutputOwningThreadId
twice in a row - and especially if you call it simultaneously on the same object from multiple threads - chances are high that it will report different thread IDs. It's a misnomer: in MTA, there ain't no such thing as an "owning thread".
Case B: you spin your explicit FactoryThread
, which creates the class factory, and then gets busy doing something (the fact that it's spinning a message pump is irrelevant in MTA; it could just as well Sleep(INFINITE)
). This thread is off-limits to COM runtime; as I said, COM can't magically interrupt it in the middle of whatever it's doing, and make it execute some COM call. So, just as in case A, all subsequent CreateInstance
and (badly named) OutputOwningThreadId
calls are executed on some threads from COM-maintained thread pool, but never on FactoryThread
.
Yes, in your approach, you are basically wasting one thread. This doesn't seem like a huge price to pay for the benefit of being able to avoid the registry.