Windows 8 introduced a new COM threading model which is a variation of the single-threaded apartment (STA). It’s called the Application STA.
The Application STA is a single-threaded apartment, but with the additional restriction that it cannot be re-entered. Consider the following timeline, with time proceeding from top to bottom:
Thread 1 | Thread 2 (normal STA) |
Thread 3 | ||
---|---|---|---|---|
Call thread 3 | → | Start operation A | ||
(waiting) | (working) | |||
Call thread 2 | → | Start operation B |
We have three threads, call them Threads 1, 2, and 3. Thread 2 makes a call to Thread 3 and waits for a response. While Thread 2 is waiting, Thread 1 makes a call into Thread 2. This causes Thread 2 to become re-entered, and that’s a tricky situation.
If the second call is to perform some operation unrelated to the first call, then you’re probably going to be okay. But suppose the second call is to perform an operation on the same object that the first call is working on. In that case, the first call will have its object modified out from under it.
void FrobAllWidgets() { for (auto&& widget : m_widgets) { widget.Frob(); // calls to thread 3 } } void AddWidget(Widget const& widget) { m_widgets.push_back(widget); }
Suppose Thread 2 is doing a FrobAllWidgets
, and the call to widget.Frob();
results in a call to Thread 3.
While one of those calls is in progress, Thread 2 is just sitting around waiting for the call to complete.
And just at that moment, Thread 1 comes in and adds another widget.
This mutates the vector of widgets, causing the for
loop in FrobAllWidgets
to go haywire because the vector was resized, causing all the iterators to become invalid.
You might try to fix this by adding a critical section:
wil::critical_section m_cs; void FrobAllWidgets() { auto guard = m_cs.lock(); for (auto&& widget : m_widgets) { widget.Frob(); // calls to thread 3 } } void AddWidget(Widget const& widget) { auto guard = m_cs.lock(); m_widgets.push_back(widget); }
You think you fixed it, but in fact nothing has changed. Critical sections support recursive acquisition, so what happens is that the re-entrant call to AddWidget
tries to acquire the critical section, and it succeeds, because the owner is the same thread.
Okay, so switch to something that does not support recursive acquisition, like a shared reader-writer lock.
wil::srwlock m_srw; void FrobAllWidgets() { auto guard = m_srw.lock_shared(); for (auto&& widget : m_widgets) { widget.Frob(); // calls to thread 3 } } void AddWidget(Widget const& widget) { auto guard = m_srw.lock_exclusive(); m_widgets.push_back(widget); }
Well, at least this time there’s no crash. Instead the call hangs, because the re-entrant call to to AddWidget
tries to acquire the exclusive lock, but it cannot because of the existing call to FrobAllWidgets
. And that existing call cannot complete until AddWidgets
completes, because AddWidgets
is running on the same stack.
FrobAllWidgets |
↳ Widget::Frob |
↳ WaitForFrobToFinish |
↳ ReceiveInboundCall |
↳ AddWidget |
AddWidget
is running on the same stack, so FrobAllWidgets
cannot return until the stack unwinds, which means that AddWidget
needs to return, but it can’t.
The Application STA tries to address this problem by blocking re-entrancy. The diagram now looks like this:
Thread 1 | Thread 2 (application STA) |
Thread 3 | ||
---|---|---|---|---|
Call thread 3 | → | Start operation A | ||
(waiting) | (working) | |||
Call thread 2 | ↴ | |||
↓ | ||||
↓ | ← | Finished | ||
↓ | Finish | |||
↳ | Operation B starts |
When Thread 1 makes a call into the Application STA while the Application STA is waiting for a response from Thread 3, the call is not allowed to proceed, because that would result in re-entrancy. The call from Thread 1 is placed on hold until Thread 2 is no longer waiting for an outbound call to complete. Once Thread 2 returns to a normal state, it can receive inbound calls again.
You can detect that you are running in an Application STA by calling CoGetApartmentType
and checking for an apartment type of STA and an apartment type qualifier of APPLICATION_
Next time, we’ll look at another quirk of the Application STA.
Bonus chatter: I forgot to include the Application STA in my list of possible results from CoGetApartmentType
, so I’ve retroactively updated it.
I can remember of something called COM-apartment's "causality" (can be read using CoGetCurrentLogicalThreadId). Was it not supposed to block re-entrant calls in the calling apartment except for the called apartment to call back into the calling apartment (sorry for using the word "call" five times in this sentence)? This sounds a lot like what Raymond described in his next article.
I remember experimenting with this many years ago (10, maybe 15) and not being able...
Coroutines make it easy for threads to mutually call each other without hard blocking and the WinRT APIs released with Windows 8 were designed for coroutines. So, I’m confused why ASTA was ever contemplated. These seem like old problems for old components that were already built with hardcoded apartment assumptions.
What does a “call to a thread” mean? A thread is just a CPU state (registers) and a Thread Information Block.
I'm not an expert on COM, but from the fact that the article shows a function called "ReceiveInboundCall", I imagine that COM has its own dispatcher. I suppose that when thread2 calls widget.Frob(), COM knows that this call should be on a different thread (because widget is bound to thread3?), and therefore somehow dispatches the call there. Then thread2 calls some COM wait function ("WaitForFrobToFinish"), which waits for either the dispatched call to end, or...