COM STA/MTA Fundamentals - Threading Models and How to Avoid Hangs

· Updated: · · COM, Windows Development, STA, MTA, Threading

Revision history (1 updates, last updated Sep 1, 2026)

A log of the changes made to this article. Where a pre-update version was archived, it stays readable at a permanent DOI link.

Retranslated as a full translation of the Japanese original. The previous English version was an abridgement that carried only part of the source, so sections, tables, Mermaid diagrams, figure captions and FAQ entries were missing. All of them have been restored to match the Japanese original, and the technical claims are the same as in the Japanese version. Read the version before this update (DOI: 10.5281/zenodo.21614451)
First published
Cite this article(DOI: 10.5281/zenodo.21614450)

This article is archived on Zenodo. Below are both the DOI that always resolves to the latest version and the DOI pinned to the version you are reading.

Go Komura (2026). COM STA/MTA Fundamentals - Threading Models and How to Avoid Hangs. KomuraSoft LLC. https://doi.org/10.5281/zenodo.21614450 https://comcomponent.com/en/blog/2026/01/31/000-sta-mta-com-relationship/

DOI (latest version)
10.5281/zenodo.21614450
DOI (this version)
10.5281/zenodo.22217116

COM’s STA/MTA is foundational knowledge that is hard to avoid when doing Windows development or touching COM from .NET. The questions people search for most often are: why is the UI thread STA, what happens when a call crosses apartments, and why do things hang?

Table of Contents


When you use COM, “which thread does this run on” is unavoidable. At the center of that question is the apartment model (STA/MTA). STA/MTA is not a general Windows threading concept - it is a threading model that determines the call rules for COM objects.

In this article, we explain the relationship between STA, MTA, and COM with diagrams, and connect it all the way through to “why things sometimes hang.”

Where the apartment model sitsDiagram showing that STA and MTA are the two forms of the apartment model, a threading model that determines the call rules for COM objects rather than a general Windows threading concept.Call rules for COM objectsApartment ModelSTAMTANot a general Windows threading concept

Figure 1: STA and MTA are the two forms of the apartment model, which determines the call rules for COM objects.

Knowledge map for this article

COM’s STA (Single-Threaded Apartment) and MTA (Multi-Threaded Apartment) are the two forms of the apartment model, which decides from which thread a component may be called. An STA holds one apartment per thread, and calls that cross an apartment boundary are marshaled through a proxy/stub. An STA that is called from another thread cannot receive the forwarded call unless it is running a message loop, so it hangs, and the pattern where a callback arrives during a synchronous call is also structurally prone to deadlock. An MTA lets several threads share a single apartment, but in exchange it requires the object itself to be designed to be thread-safe. The [STAThread] attribute in .NET is nothing more than a wrapper that configures this apartment initialization.

COM STA/MTA apartment modelsDiagram showing how the STA and MTA apartment models of COM relate to the message loop, marshaling, and the proxy/stub, and how the absence of a message loop leads to a hang or a deadlock in an STAusesrequiresrequiresusesrequiresusesusesusesrequiresusesconfigured bymay causepreventspreventsmay causemay causeusesrequiresimplementsimplementsSTA (Single-Threaded Apartment)MTA (Multi-Threaded Apartment)COM (Component Object Model)COM apartment model (STA/MTA)CoInitializeExMessage LoopProxy/StubThread-Safe COM Object DesignWindows Forms[STAThread] / [MTAThread] AttributesThreadingModel Registry ValueOLE AutomationIDispatchMIDLSTA Hang (Stalled Call Marshaling)MsgWaitForMultipleObjectsCallback Deadlock During Synchronous Call.NET (Core and Later)Marshaling

In the diagram a solid line marks a relation that always holds and a dashed line marks a conditional one (the conditions are given per relation on the detail page). The full list of relations (20 in total, with evidence and certainty) and the definitions of the main concepts are collected on the knowledge map detail page (in Japanese). Data: JSON-LD / Turtle

1. The Conclusion First (In One Line)

  • A COM object’s call rules are determined by which apartment it belongs to
  • It is easiest to think of STA as one apartment per thread, and MTA as one apartment shared by multiple threads
  • For calls that cross apartments, COM marshals them through a proxy/stub
How STA, MTA, and cross-apartment calls relateDiagram showing the conclusion that STA takes the form of one apartment per thread and MTA the form of one apartment shared by multiple threads, and that COM marshals cross-apartment calls through a proxy/stub.Call across apartmentsCall across apartmentsSTA (one apartment per thread)Marshaled through a proxy / stubMTA (one apartment for multiple threads)

Figure 2: The apartment an object belongs to decides its call rules, and marshaling is inserted only when a call crosses apartments.

2. Call Patterns in the Apartment Model (Diagrams)

There are broadly three patterns for calling a COM object.

The three call patternsDiagram showing that calls to a COM object fall into three patterns: calls within the same STA thread, calls within the same MTA, and calls that cross apartments.Calling a COM objectPattern 1: within the same STA threadPattern 2: within the same MTAPattern 3: across apartments

Figure 3: Calls split into three patterns, and which one applies changes both the overhead and what you have to watch for.

2.1. Pattern 1: Calls Within the Same STA Thread

Within the same STA thread, calls are direct. No overhead.

STA threadDirect callCalling codeCOM object

Figure 4: A call within the same STA thread is a direct call with no overhead.

2.2. Pattern 2: Calls Within the Same MTA

From multiple threads inside the MTA, any thread can call directly. However, the object itself must be designed to be thread-safe.

MTA - one apartmentDirect callDirect callWorker thread 1COM objectWorker thread 2

Figure 5: Within the same MTA any thread can call directly, but the object needs a thread-safe design.

2.3. Pattern 3: Calls Across Apartments

Between different apartments, COM forwards the call using a proxy/stub. For standard interfaces, the COM runtime handles this for you.

The table further down uses COM-specific vocabulary without explaining it, so here is one line for each term first.

Term Meaning
Marshaling Repackaging a call and its arguments into a form that can be handed across an apartment or process boundary, and carrying them over. They are reconstructed into their original form on the other side
Proxy / stub The pair of components responsible for marshaling. The proxy stands on the calling side (it impersonates the real object and receives the call), and the stub stands on the called side (it passes the received call to the real object)
IDispatch The COM interface for looking up a method name as a string and then invoking it by number. This mechanism is what lets scripting languages and VBA use COM
Automation The umbrella term for using COM on top of IDispatch and the limited set of data types available there (BSTR, VARIANT, and so on). As long as you stay within that range, the OS handles marshaling through oleaut32.dll
Type library Machine-readable data that describes the shape of an interface (its methods and argument types). It ships either as a standalone .tlb file or embedded in a DLL or EXE
Type library marshaler The standard COM facility that reads a type library and marshals on the spot. It is the reason you can get by without building a dedicated proxy/stub
MIDL Microsoft’s compiler that generates proxy/stub code and similar output from an interface definition (.idl)

Note: Proxies/stubs are not automatically available for everything, but in practice you rarely need to generate them explicitly.

Pattern Proxy/stub preparation
IDispatch-based (Automation) Not needed. oleaut32.dll handles it
Type library registered Not needed. The type library marshaler handles it
.NET COM Interop Usually not needed. Works via the type library
Custom interface deriving directly from IUnknown Proxy/stub generation and registration via MIDL required

In other words, you only need MIDL-generated proxies/stubs when you create an interface that derives directly from IUnknown without using IDispatch. For typical COM components consumed from .NET or scripting languages, this work is rarely necessary.

When you have to prepare a proxy/stub yourselfDiagram showing that no proxy/stub generation is needed when standard COM marshaling covers the case, such as IDispatch-based interfaces or the type library marshaler, and that MIDL generation and registration is required only for a custom interface derived directly from IUnknown that standard marshaling cannot cover.YesNoCan standard marshaling cover itNo proxy/stub generation neededGenerate and register a proxy/stub with MIDLIDispatch or the type library handles itThis is nearly always the case in practice

Figure 6: You generate a proxy/stub explicitly only for a custom interface derived directly from IUnknown that standard marshaling cannot cover.

MTA threadCOM runtime - automaticSTA threadCallForwardCOM objectProxyRPC/IPCStubCalling code

Figure 7: A call that crosses apartments is forwarded through the proxy, RPC, and stub that the COM runtime sets up automatically.

Key point: Crossing apartments incurs marshaling overhead. For high-frequency calls this affects performance, so it needs to be considered at design time.

2.4. Rough Marshaling Overhead Figures

The following are general ballpark figures (not measured values; they vary greatly depending on the situation and parameter complexity).

Call pattern Rough time Relative feel
Same apartment (direct) 10-100 nanoseconds About the same as a normal function call
Different apartments (same process) 1-10 microseconds 100-1000x a direct call
Different processes (out-of-proc) 100-1000 microseconds 10,000-100,000x a direct call

Relative comparison:

  • Same apartment: about one memory access
  • Different apartments: about one system call
  • Different processes: about a network round trip to localhost

In a scenario like calling 10,000 times in a loop, this difference becomes very noticeable.

How to treat these numbers

The table above is there to give you a feel for the order of magnitude; it is not a sourced measurement. There is no published standard benchmark for this either, so do not use the numbers themselves as the basis for a design decision.

What is documented, on the other hand, is the structure: calls inside the same apartment are direct, and crossing a boundary always inserts marshaling - that part is specified in Microsoft’s documentation. The Single-Threaded Apartments topic states explicitly that an interface pointer can be passed within the same apartment without marshaling, that crossing apartments uses the same marshaling machinery as a cross-process call even inside a single process, and that the call arrives as a window message to a hidden window (window class OleMainThreadWndClass). That structure is why the order of magnitude changes, while the numbers themselves move with the environment and the complexity of the arguments.

When you want a judgment for your own case, the reliable approach is to measure the same interface called from within the same apartment and from a different one, and compare the two.

The structural reason the order of magnitude changesDiagram showing the structural reason the order of magnitude changes: a call inside the same apartment is a direct call, while a call across apartments always inserts marshaling even within a single process, and a call bound for an STA arrives as a window message to a hidden window.Call inside the same apartmentDirect callCall across apartmentsMarshaling is always insertedCalls bound for an STA arrive at a hidden window

Figure 8: The order of magnitude changes because of the specified structure - crossing a boundary always inserts marshaling.

// C#. Repeat the same call N times and derive the time per call
static void Measure(string label, Action call, int iterations = 100_000)
{
    call(); // Keep the first-call latency (JIT, proxy creation, connection setup) out of the measurement

    var sw = System.Diagnostics.Stopwatch.StartNew();
    for (int i = 0; i < iterations; i++)
    {
        call();
    }
    sw.Stop();

    double perCallNs = sw.Elapsed.TotalMilliseconds * 1_000_000.0 / iterations;
    Console.WriteLine($"{label}: {perCallNs:F1} ns/call");
}

Measuring both a method with few arguments and a method that passes strings or arrays also shows how the impact scales with the amount of marshaling.

3. STA (Single-Threaded Apartment)

STA is the “one thread = one apartment” model.

  • COM objects in that apartment fundamentally execute only on that thread
  • When called from another thread, COM forwards the call via the message queue/RPC
  • Commonly used on UI threads (WinForms/WPF) - the UI also has “single-thread affinity plus a message loop,” so the fit is natural
The STA execution modelDiagram showing that in an STA a COM object fundamentally executes only on the thread that created it, and that COM forwards calls from other threads to that thread via the message queue or RPC.COM object in an STAExecutes only on the thread that created itCall from another threadForwarded via the message queue / RPC

Figure 9: An STA object runs only on its owning thread, and calls from elsewhere reach it by being forwarded.

3.1. Why STA Is Used on UI Threads

Because the UI thread and STA share the same design.

  • UI controls are not thread-safe Buttons, text boxes, and so on can only be safely manipulated from the thread that created them
  • STA likewise has “single-thread affinity” COM objects execute directly only on the thread that created them
  • The UI thread always pumps a message loop This is required to handle window events, and matches STA’s prerequisite (a message pump)

That is why the UI thread in WinForms/WPF is STA by default.

How the UI thread and STA designs line upDiagram showing that UI controls can only be manipulated safely from the thread that created them, that STA has the same single-thread affinity, and that the message loop a UI thread always pumps matches STA's prerequisite of a message pump, which is why the UI thread is STA by default.Single-thread affinity of UI controlsThe designs line upSingle-thread affinity of STAMessage loop on the UI threadSTA prerequisite of a message pumpThe UI thread is STA by default

Figure 10: The designs line up on two points, single-thread affinity and the message loop, which is why UI threads are STA.

Key point: STA gives you strong thread affinity, but in exchange it tends to become congested when there are many callers.

4. MTA (Multi-Threaded Apartment)

MTA is the “multiple threads in one apartment” model.

  • COM objects get called concurrently from multiple threads
  • The object must be designed to be thread-safe
  • Suited to server-side and background processing

Key point: MTA gives high parallelism, but places a heavy burden on the object’s implementation.

The MTA execution modelDiagram showing that an MTA shares one apartment across multiple threads, so a COM object is called concurrently from several threads and the object itself must be designed to be thread-safe.MTA (one apartment for multiple threads)Called concurrently from multiple threadsThe object must be designed to be thread-safeSuited to server-side and background processing

Figure 11: MTA lets you call in parallel, but shifts the responsibility for safety onto the object implementation.

5. Where STA/MTA Gets Decided

A COM apartment is decided by initializing each thread.

  • The moment you call CoInitialize / CoInitializeEx, that thread’s apartment is decided
  • STA: COINIT_APARTMENTTHREADED
  • MTA: COINIT_MULTITHREADED
The moment an apartment is decidedDiagram showing that a thread's apartment is decided the moment it calls CoInitialize or CoInitializeEx, becoming an STA with COINIT_APARTMENTTHREADED and an MTA with COINIT_MULTITHREADED.COINIT_APARTMENTTHREADEDCOINIT_MULTITHREADEDThreadCalls CoInitialize / CoInitializeExDecided as STADecided as MTA

Figure 12: An apartment is decided per thread, by what you pass the moment initialization is called.

5.1. STA/MTA in .NET

.NET also has the [STAThread] / [MTAThread] attributes and ApartmentState, but these are wrappers for configuring COM’s apartment model.

  • [STAThread]applied to the Main method (entry point). The thread is initialized as STA when COM is used
  • [MTAThread] → likewise for the Main method. Initialized as MTA
  • Thread.SetApartmentState(ApartmentState.STA)for additional threads you create. Must be set before the thread starts

Caveats:

  • Even with [STAThread], nothing is initialized until COM is actually used (it has no effect if you never touch COM)
  • [STAThread] has no effect on additional threads. Use Thread.SetApartmentState

In other words, .NET’s STA/MTA is COM’s STA/MTA itself - a mechanism provided for COM Interop.

Important: You cannot change an apartment afterwards. The first initialization is everything.

Where you set the apartment in .NETDiagram showing that the Main method carries the STAThread or MTAThread attribute while additional threads are set with Thread.SetApartmentState before they start, and that in both cases the apartment is fixed by the first initialization and cannot be changed afterwards.Main methodSTAThread / MTAThread attributeAdditional thread you createThread.SetApartmentState before startThe apartment is fixed by the first initializationCannot be changed afterwards

Figure 13: The entry point uses an attribute and additional threads use SetApartmentState, and both are settled by that first call.

6. A Concrete Example of a Hang Caused by Getting STA Wrong

A setup like the following is genuinely prone to hangs.

6.1. The Typical Situation

  • A background STA thread is created and a COM object is instantiated on it
  • That thread is not pumping a message loop
  • Another thread (whether STA or MTA) calls that COM object
The setup that tends to hangDiagram showing the hang-prone setup where a background STA thread creates a COM object but is not pumping a message loop, and another thread calls that COM object.CallsBackground STA threadCreates a COM objectNot pumping a message loopAnother thread (STA or MTA)

Figure 14: The dangerous combination is calling an object owned by a non-pumping STA from another thread.

6.2. What Happens

The reason for the hang comes down to two STA prerequisites. This section is where that reason is explained; the sections that follow do not repeat it.

  • A COM object is processed on the STA thread that created it Whether the caller is STA or MTA, a call from another thread is always forwarded to that STA thread. It arrives as a window message to the hidden window COM creates for that apartment (window class OleMainThreadWndClass)
  • To receive that forwarded call, the STA thread has to be pumping messages Microsoft’s documentation states it plainly: every STA must have a message loop in order to handle calls from other processes and from other apartments in the same process

So an STA thread that is not pumping messages cannot receive the call, the caller keeps waiting for a reply, and the result is a hang.

The path to a hangDiagram showing that a call from another thread is forwarded to the STA thread that created the object as a window message to a hidden window, but the STA cannot receive it while no message pump is running, so the caller keeps waiting for a reply and the process hangs.Call from another threadForwarded to the STA thread that created the objectArrives as a message to the hidden windowNo message pump is runningThe STA side cannot receive the callThe caller keeps waiting for a replyHang

Figure 15: The forwarded call arrives as a message, so on an STA whose pump has stopped there is nobody left to receive it.

The same holds in .NET. The Single-Threaded Apartments documentation carries a warning that blocking an STA thread with Task.Wait(), Task.Result, Thread.Sleep(), ManualResetEvent.WaitOne(), and the like prevents COM callbacks and cross-apartment calls from completing, which deadlocks. The failure example in 6.3 below is stuck in WaitOne() for exactly this reason.

A UI thread, by contrast, pumps a message loop from the start in order to handle window events, so it satisfies STA’s requirement with no extra implementation. That is why the UI thread is a natural place to run an STA COM object.

6.3. Pseudocode (The Classic Failure Pattern)

using System;
using System.Runtime.InteropServices;
using System.Threading;

internal static class StaHangDemo
{
    private const uint COINIT_APARTMENTTHREADED = 0x2;

    [DllImport("ole32.dll")]
    private static extern int CoInitializeEx(IntPtr pvReserved, uint dwCoInit);

    [DllImport("ole32.dll")]
    private static extern void CoUninitialize();

    public static void Run(string progId)
    {
        var ready = new AutoResetEvent(false);
        var done = new AutoResetEvent(false);

        object comObj = null;

        var staThread = new Thread(() =>
        {
            // Initialize as STA
            CoInitializeEx(IntPtr.Zero, COINIT_APARTMENTTHREADED);

            // Assumes a COM class registered with ThreadingModel=Apartment
            Type type = Type.GetTypeFromProgID(progId, throwOnError: true);
            comObj = Activator.CreateInstance(type);
            ready.Set();

            // Waiting with no message loop -> this is the fatal flaw
            done.WaitOne();

            CoUninitialize();
        });

        staThread.SetApartmentState(ApartmentState.STA);
        staThread.Start();

        ready.WaitOne();

        try
        {
            // Calling from another thread (STA or MTA) forwards the call to the STA
            // The STA side does not process messages, so this is where it tends to hang
            dynamic obj = comObj;
            obj.AnyMethod();
        }
        finally
        {
            // Even when the call does return - the class turned out to be agile,
            // the call was not forwarded, AnyMethod returned immediately - always
            // release the STA thread. Skip this and a foreground thread is left
            // waiting on done, so the process never exits even when the hang did
            // not reproduce, and the symptom no longer tells the two cases apart
            done.Set();
            staThread.Join();
        }
    }
}

This code assumes you pass the ProgID of a COM class registered with ThreadingModel=Apartment (that is, STA). Replace AnyMethod with a method the class actually has. It does not reproduce with every COM class. Three conditions have to hold.

Condition How to check
The target class has ThreadingModel set to Apartment Look at the ThreadingModel value under HKEY_CLASSES_ROOT\CLSID\{CLSID}\InprocServer32 in the registry. With Both or Free the call is not forwarded, so it does not reproduce
The STA thread is not pumping messages done.WaitOne() in the example above is exactly that
The call is made from a different thread As long as you call from inside the same STA thread it is a direct call, so it does not hang

obj.AnyMethod() is wrapped in try / finally so that done.Set() and staThread.Join() always run, and the point of that is to tell the non-reproducing case apart. An STA thread is a foreground thread by default, so the process does not exit until done is signaled. Drop the finally and the externally visible symptom becomes the same - the process never exits - both when the hang reproduces (the call never returns) and when it does not (the call returned, but nobody signals done). The tool you built to confirm the reproduction conditions ends up hiding whether they were met. With the form above, a run that does not reproduce simply exits normally.

The quickest way to confirm that it is stuck is to break into the process with a debugger and look at the stacks: the calling thread parked in a COM wait, and the STA thread parked in WaitOne.

COM runtimeSTA threadMain threadCOM runtimeSTA threadMain threadNo message loopStuck right hereForwards via a message, but...Stuck in WaitOne, socannot process messagesThe caller keeps waiting tooBoth are waiting → hangStart threadCoInitializeEx (STA)Create COM objectready.Set()Waiting on done.WaitOne()CallComObject()Tries to forward the call

Figure 16: The STA thread stuck in WaitOne and the main thread waiting for the forwarded call can no longer make progress.

The two lines in the middle of the diagram, around “Forwards via a message, but…”, are where the two prerequisites from 6.2 break down.

6.4. Key Points for Avoiding It

  • If the STA thread will receive calls from other threads, it must pump a message loop
  • If possible, create and use the object on the UI thread (which has a message loop from the start)
  • If you do not need STA, use MTA from the beginning

Note: If everything stays within a single thread, Application.Run() is not always required. However, UI-related and COM-related code so often involves calls from other threads that in practice it is nearly mandatory.

Three directions for avoiding the hangDiagram showing three directions for avoiding the hang: pump a message loop on the STA thread when it receives calls from other threads, create and use the object on the UI thread which has a message loop from the start, or use MTA from the beginning when STA is not required.How to avoid an STA hangPump a message loop on the STA threadCreate and use the object on the UI threadUse MTA from the beginning if STA is not requiredThe UI thread has a loop from the start

Figure 17: The workarounds sort into three directions: pump the loop, move to where a loop already runs, or stop using STA.

6.5. What Does “Pumping the Message Loop” Actually Mean?

It is this familiar pattern that every Win32 UI thread runs.

while (GetMessage(out var msg, IntPtr.Zero, 0, 0))
{
    TranslateMessage(ref msg);
    DispatchMessage(ref msg);
}

In STA, calls from other threads arrive as “forwarded” work. This loop (the message pump) is what receives that forwarded work and dispatches it for execution.

What the message pump doesDiagram showing that the message pump is the repeated cycle of receiving a forwarded call with GetMessage and dispatching it for execution with DispatchMessage.Forwarded callReceived by GetMessageDispatched for execution by DispatchMessage

Figure 18: Pumping the message loop means exactly this cycle: receive the forwarded call and dispatch it for execution.

6.6. An Example in the Right Direction (Roughly Sketched)

If you want “COM on a background STA,” it looks like this.

var ready = new AutoResetEvent(false);
object comObj = null;

var staThread = new Thread(() =>
{
    CoInitializeEx(IntPtr.Zero, COINIT_APARTMENTTHREADED);

    comObj = new SomeStaComObject();
    ready.Set();

    // Pump messages for as long as the STA thread is alive
    Application.Run();

    CoUninitialize();
});

staThread.SetApartmentState(ApartmentState.STA);
staThread.Start();

ready.WaitOne();
CallComObject(comObj);

(Note: forgetting to call CoInitializeEx / CoUninitialize is a very real way to hurt yourself. You need the same CoInitializeEx P/Invoke declaration as in 6.3.)

The parameterless form of Application.Run() exists to pump a message loop without owning a form. This is exactly the situation it is meant for, but keep the following two points in mind.

  • You need a reference to System.Windows.Forms. In a console app, add <UseWindowsForms>true</UseWindowsForms> to the project file
  • This loop never ends on its own. To stop it, call Application.ExitThread() on that thread (or Application.Exit() to end the whole application). If you want the example above to reach CoUninitialize(), you need a separate mechanism that signals shutdown to the STA thread and gets it to call ExitThread

If you would rather not depend on WinForms, write the GetMessage / DispatchMessage loop from 6.5 yourself, or use MsgWaitForMultipleObjects, which can wait on both messages and synchronization objects. The latter is the standard choice when you want to receive a shutdown signal through an event without dropping any COM calls.

Three ways to pump a loop on a background STADiagram showing three ways to pump a message loop on a background STA: Application.Run which requires a WinForms reference, writing the GetMessage and DispatchMessage loop yourself, and MsgWaitForMultipleObjects which can wait on both messages and synchronization objects.Pump a loop on a background STAApplication.RunWrite the GetMessage loop yourselfMsgWaitForMultipleObjectsNeeds a WinForms reference and a way to stopCan wait on both messages and synchronization objects

Figure 19: There are three ways to run the loop, and you pick one by how it stops and how heavy the dependency is.

6.7. Another Hang Example: Callbacks During a Synchronous Call

STA is not just about “calls getting forwarded” - depending on the situation, callbacks come back the other way (server to client). Among them, a callback arriving during a synchronous call is the classic deadlock.

COM serverUI thread (STA)COM serverUI thread (STA)Waiting for DoWork to return(not processing messages)Waiting, socannot receive the callbackWaiting for the callback to completeEach is waiting on the other → deadlockDoWork() (synchronous call)ProgressCallback() (callback)

Figure 20: The UI thread waiting for a synchronous call to return and the server waiting for the callback to finish end up waiting on each other.

Why this deadlocks so easily:

  1. The UI thread makes a synchronous (blocking) call to DoWork()
  2. The UI thread is waiting for the return (not processing messages)
  3. The server sends ProgressCallback() to the UI thread
  4. The UI thread is waiting, so it cannot receive the callback
  5. The server is waiting for the callback to complete
  6. Each side is waiting on the other, so nothing ever moves

How long the processing takes is irrelevant. The pattern itself - a callback arriving during a synchronous call - is what causes trouble.

Note: COM does have mechanisms that pump messages or allow reentrancy in some situations, and the behavior varies by component and call style. It does not always deadlock, but this pattern is best avoided.

7. A Rough Guide to Choosing

Situation What to choose Why What else you need
UI is involved (WinForms / WPF) STA UI controls have single-thread affinity, and the UI thread has a message loop to begin with (3.1) Nothing in particular. It is STA by default
You want heavy parallel processing MTA Multiple threads share one apartment and can call directly with no forwarding (2.2) A thread-safe design in the COM object. This is the object implementation’s responsibility, not locking on the caller’s side
You want to use an STA COM object in the background STA plus a message loop Since it is called from another thread, it needs somewhere to receive the forwarded call (6.2) Application.Run() or a GetMessage loop. Design the shutdown path (ExitThread and so on) along with it (6.6)
The COM component you use already dictates the answer Match the component The apartment is the call rule itself, and it cannot be changed later (Section 5) Check the ThreadingModel value. If it is Apartment, build on the assumption of STA
You call it at high frequency Move it into the caller’s apartment Marshaling is inserted every time a boundary is crossed (2.4) If that is impractical, design for fewer calls instead by batching arguments into one call

When you are unsure, the order component requirements > whether UI is involved > parallelism usually settles it. An apartment is fixed by the first initialization and cannot be changed later, so this is the one decision to make before you start implementing.

The order to check when you are unsureDiagram showing that when you are unsure whether to use STA or MTA, checking the requirements of the COM component you use, then whether UI is involved, then parallelism, usually settles it.thenthenRequirements of the componentWhether UI is involvedParallelism

Figure 21: When the choice is unclear, check the component’s requirements, then whether UI is involved, then parallelism.

8. Conclusion

STA/MTA is the threading model for COM: STA takes the form of one thread = one apartment, and MTA puts multiple threads in one apartment. Calls that cross apartments are forwarded by COM via proxies/stubs (interfaces outside the standard set need generation and registration with MIDL or the like), but this comes with marshaling overhead, so apartment design deserves careful thought wherever high-frequency calls are expected.

From the hang perspective, it all comes down to one point: an STA thread that receives calls from other threads is expected to pump messages. Calling into an STA thread that is not pumping messages is likely to hang, and the pattern where a callback arrives during a synchronous call easily deadlocks. The UI thread has both “single-thread affinity” and “a message loop” from the start, satisfying these prerequisites with no extra implementation - which is exactly why it gets along so well with STA COM.

9. References

  • Apartment Model https://learn.microsoft.com/en-us/windows/win32/com/com-apartments
  • CoInitializeEx https://learn.microsoft.com/en-us/windows/win32/api/objbase/nf-objbase-coinitializeex
  • Single-Threaded Apartments (why a message loop is required, the hidden window, and the deadlock warning for .NET) https://learn.microsoft.com/en-us/windows/win32/com/single-threaded-apartments
  • Multithreaded Apartments https://learn.microsoft.com/en-us/windows/win32/com/multithreaded-apartments
  • InprocServer32 (the ThreadingModel values) https://learn.microsoft.com/en-us/windows/win32/com/inprocserver32
  • Application.Run method (pumping a message loop without a form, and how to stop it) https://learn.microsoft.com/en-us/dotnet/api/system.windows.forms.application.run
  • MsgWaitForMultipleObjects (waiting on messages and synchronization objects at the same time) https://learn.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-msgwaitformultipleobjects

Download the Word version of this article

Recent articles sharing the same tags. Deepen your understanding with closely related topics.

These topic pages place the article in a broader service and decision context.

This article connects naturally to the following service pages.

Frequently Asked Questions

Common questions about the topic of this article.

Should I choose STA or MTA?
The basic split is STA when UI is involved and MTA for heavy parallel processing. STA has strong thread affinity because it takes the form of one apartment per thread, but it tends to become congested when there are many callers. MTA shares one apartment across multiple threads, so it offers more parallelism, but the COM object itself must be designed to be thread-safe. When neither case applies, the practical answer is to follow whatever the existing libraries or COM servers you use require.
Why is the UI thread STA?
Because the UI thread and STA share the same design. UI controls such as buttons and text boxes are not thread-safe and can only be manipulated safely from the thread that created them, and STA is likewise a single-thread-affinity model. On top of that, a UI thread always pumps a message loop to handle window events, so it satisfies STA's prerequisite of a message pump with no extra implementation. That is why UI threads in WinForms and WPF are STA by default.
Why does calling an STA COM object hang?
A call to an STA COM object is processed on the STA thread that created it. COM forwards calls from other threads via messages or RPC, but an STA thread that is not pumping a message loop cannot receive the forwarded call, so the caller keeps waiting and hangs. To avoid this, pump a message loop on any STA thread that receives calls from other threads, create and use the object on the UI thread, or use MTA from the start if you do not need STA.
What is the .NET [STAThread] attribute for?
It is a wrapper for configuring COM's apartment model. Applied to the Main method, it makes that thread initialize as STA when COM is used. Nothing is initialized until you actually call into COM, however, so it has no effect in an application that never touches COM. It also has no effect on additional threads you create, so set those with Thread.SetApartmentState before the thread starts. Note as well that an apartment is decided by the first initialization and cannot be changed afterwards.

Author Profile

Profile page for the article author.

Go Komura

Representative of KomuraSoft LLC

Focused on Windows software development, technical consulting, and investigations into failures that are difficult to reproduce.

Back to the Blog