February 24th, 2021

What is so special about the Application STA?

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 Frob­All­Widgets, 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 Frob­All­Widgets 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 Add­Widget 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 Add­Widget tries to acquire the exclusive lock, but it cannot because of the existing call to Frob­All­Widgets. And that existing call cannot complete until Add­Widgets completes, because Add­Widgets is running on the same stack.

FrobAllWidgets
↳ Widget::Frob
↳ WaitForFrobToFinish
↳ ReceiveInboundCall
↳ AddWidget

Add­Widget is running on the same stack, so Frob­All­Widgets cannot return until the stack unwinds, which means that Add­Widget 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 Co­Get­Apartment­Type and checking for an apartment type of STA and an apartment type qualifier of APPLICATION_STA.

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 Co­Get­Apartment­Type, so I’ve retroactively updated it.

Topics
Code

Author

Raymond has been involved in the evolution of Windows for more than 30 years. In 2003, he began a Web site known as The Old New Thing which has grown in popularity far beyond his wildest imagination, a development which still gives him the heebie-jeebies. The Web site spawned a book, coincidentally also titled The Old New Thing (Addison Wesley 2007). He occasionally appears on the Windows Dev Docs Twitter account to tell stories which convey no useful information.

4 comments

Discussion is closed. Login to edit/delete existing comments.

  • Vanja Kralj

    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...

    Read more
  • switchdesktopwithfade@hotmail.com

    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.

  • Piotr Siódmak

    What does a “call to a thread” mean? A thread is just a CPU state (registers) and a Thread Information Block.

    • Jonathan BarnerMicrosoft employee

      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...

      Read more