Grundlagenwissen zu COM STA/MTA – Threadmodelle und wie man Hänger vermeidet

· · COM, Windows-Entwicklung, STA, MTA, Threads

STA/MTA bei COM ist Grundlagenwissen, das sich bei der Windows-Entwicklung oder beim Umgang mit COM aus .NET heraus kaum umgehen lässt. Besonders häufig gesucht werden die Fragen, warum der UI-Thread STA ist, was beim Überschreiten von Apartment-Grenzen passiert, und warum es zu Hängern kommt.

Inhaltsverzeichnis

  1. Zuerst das Fazit (in Kürze)
  2. Aufrufmuster im Apartment-Modell (Diagramm)
    • 2.1. Muster 1: Aufruf innerhalb desselben STA-Threads
    • 2.2. Muster 2: Aufruf innerhalb derselben MTA
    • 2.3. Muster 3: Aufruf über Apartment-Grenzen hinweg
    • 2.4. Richtwerte für den Marshalling-Overhead
  3. STA (Single-Threaded Apartment)
    • 3.1. Warum STA auf UI-Threads verwendet wird
  4. MTA (Multi-Threaded Apartment)
  5. Wo STA/MTA festgelegt wird
    • 5.1. STA/MTA in .NET
  6. Ein konkretes Beispiel für einen Hänger durch falsches STA
    • 6.1. Eine typische Situation
    • 6.2. Was dabei passiert
    • 6.3. Pseudocode (das typische Fehlermuster)
    • 6.4. Die wichtigsten Punkte zur Vermeidung
    • 6.5. Was bedeutet „eine Nachrichtenschleife betreiben“ eigentlich?
    • 6.6. Ein Beispiel in die richtige Richtung (grob skizziert)
    • 6.7. Ein weiteres Hänger-Beispiel: Callback während eines synchronen Aufrufs
  7. Grobe Entscheidungshilfe
  8. Zusammenfassung
  9. Referenzen

Beim Einsatz von COM lässt sich die Frage „auf welchem Thread läuft das eigentlich“ nicht umgehen. Im Zentrum dieser Frage steht das Apartment-Modell (STA/MTA). STA/MTA ist kein allgemeines Windows-Threadkonzept, sondern ein Threadmodell, das die Aufrufregeln für COM-Objekte festlegt.

Dieser Artikel erklärt den Zusammenhang zwischen STA, MTA und COM anhand von Diagrammen und führt bis zu der Frage, „warum es manchmal zu Hängern kommt“.

1. Zuerst das Fazit (in Kürze)

  • Bei einem COM-Objekt bestimmt die Zugehörigkeit zu einem bestimmten Apartment die Aufrufregeln
  • Am einfachsten lässt sich STA als ein Apartment pro Thread und MTA als ein Apartment für mehrere Threads verstehen
  • Aufrufe über Apartment-Grenzen hinweg marshalt COM über Proxy/Stub

2. Aufrufmuster im Apartment-Modell (Diagramm)

Beim Aufruf eines COM-Objekts gibt es grob drei Muster.

2.1. Muster 1: Aufruf innerhalb desselben STA-Threads

Innerhalb desselben STA-Threads ist ein direkter Aufruf möglich. Kein Overhead.

STA-ThreadDirekter AufrufAufrufender CodeCOM-Objekt

2.2. Muster 2: Aufruf innerhalb derselben MTA

Aus mehreren Threads innerhalb der MTA kann jeder Thread direkt aufrufen. Das Objekt selbst muss dabei aber threadsicher entworfen sein.

MTA - ein ApartmentDirekter AufrufDirekter AufrufWorker-Thread 1COM-ObjektWorker-Thread 2

2.3. Muster 3: Aufruf über Apartment-Grenzen hinweg

Zwischen unterschiedlichen Apartments leitet COM über Proxy/Stub weiter. Bei Standardschnittstellen übernimmt das die COM-Runtime.

In der folgenden Tabelle tauchen COM-spezifische Begriffe ohne weitere Erklärung auf. Deshalb hier vorab je eine Zeile dazu.

Begriff Bedeutung
Marshalling Beim Überschreiten von Apartment- oder Prozessgrenzen werden Aufruf und Argumente in eine „unverändert übertragbare Form“ umgepackt und transportiert. Auf der anderen Seite der Grenze werden sie wieder in ihre ursprüngliche Form zurückverwandelt
Proxy / Stub Ein zusammengehöriges Paar von Bauteilen, das das Marshalling übernimmt. Auf der Aufruferseite steht der Proxy (er gibt sich als das echte Objekt aus und nimmt den Aufruf entgegen), auf der aufgerufenen Seite der Stub (er gibt den empfangenen Aufruf an das echte Objekt weiter)
IDispatch Eine COM-Schnittstelle, die Methodennamen als Zeichenkette entgegennimmt und über eine Nummer aufruft. Dieser Mechanismus ist der Grund, warum sich COM aus Skriptsprachen oder VBA heraus verwenden lässt
Automation Sammelbegriff für die Verwendung von COM auf Basis von IDispatch und den dort verfügbaren, eingeschränkten Datentypen (BSTR, VARIANT usw.). Bleibt man in diesem Rahmen, übernimmt oleaut32.dll auf Betriebssystemseite das Marshalling
Typbibliothek Maschinenlesbare Daten, die die Form einer Schnittstelle (Methoden, Parametertypen) beschreiben. Sie liegt entweder als eigenständige .tlb-Datei oder eingebettet in eine DLL/EXE vor
Typbibliotheks-Marshaller Eine COM-Standardfunktion, die die Typbibliothek liest und das Marshalling zur Laufzeit direkt durchführt. Ihr ist es zu verdanken, dass man keinen eigenen Proxy/Stub erstellen muss
MIDL Der Compiler von Microsoft, der aus einer Schnittstellendefinition (.idl) unter anderem den Code für Proxy/Stub generiert

Hinweis: Proxy/Stub steht nicht für alles automatisch bereit, in der Praxis muss man sie aber in den meisten Fällen nicht explizit generieren.

Muster Proxy/Stub-Vorbereitung
IDispatch-basiert (Automation) Nicht nötig. Übernimmt oleaut32.dll
Typbibliothek registriert Nicht nötig. Übernimmt der Typbibliotheks-Marshaller
.NET COM Interop Normalerweise nicht nötig. Läuft über die Typbibliothek
Benutzerdefinierte Schnittstelle, direkt von IUnknown abgeleitet Proxy/Stub-Generierung und -Registrierung per MIDL erforderlich

Mit anderen Worten: Eine Proxy/Stub-Generierung per MIDL wird nur benötigt, wenn eine Schnittstelle erstellt wird, die direkt von IUnknown abgeleitet ist, ohne IDispatch zu verwenden. Bei üblichen COM-Komponenten, die aus .NET oder Skriptsprachen heraus verwendet werden, ist dieser Schritt selten nötig.

MTA-ThreadCOM-Runtime - automatischSTA-ThreadAufrufWeiterleitungCOM-ObjektProxyRPC/IPCStubAufrufender Code

Wichtig: Beim Überschreiten von Apartment-Grenzen entsteht Marshalling-Overhead. Bei häufigen Aufrufen wirkt sich das auf die Performance aus, weshalb dies bereits beim Entwurf berücksichtigt werden sollte.

2.4. Richtwerte für den Marshalling-Overhead

Die folgenden Werte sind allgemeine Richtwerte (keine Messwerte – sie schwanken stark je nach Situation und Komplexität der Parameter).

Aufrufmuster Richtwert für die Zeit Relatives Gefühl
Innerhalb desselben Apartments (direkt) 10–100 Nanosekunden Praktisch wie ein gewöhnlicher Funktionsaufruf
Unterschiedliche Apartments (gleicher Prozess) 1–10 Mikrosekunden 100–1000-mal so lang wie ein direkter Aufruf
Unterschiedliche Prozesse (Out-of-proc) 100–1000 Mikrosekunden 10.000–100.000-mal so lang wie ein direkter Aufruf

Relativer Vergleich:

  • Gleiches Apartment: etwa wie ein Speicherzugriff
  • Unterschiedliche Apartments: etwa wie ein Systemaufruf
  • Unterschiedliche Prozesse: etwa wie Netzwerkkommunikation zu localhost

In einem Szenario, in dem eine Schleife 10.000-mal aufruft, macht sich dieser Unterschied deutlich bemerkbar.

Zum Umgang mit diesen Zahlen

Die obige Tabelle dient nur dazu, ein Gefühl für die Größenordnung zu vermitteln, und ist kein belegter Messwert. Da es auch keine öffentlich verfügbare, einheitliche Benchmark-Referenz gibt, sollten Sie diese Zahlen selbst nicht als Grundlage für Entwurfsentscheidungen heranziehen.

Dagegen ist die Struktur „innerhalb desselben Apartments direkter Aufruf, beim Überschreiten der Grenze wird immer Marshalling zwischengeschaltet“ eine in der Microsoft-Dokumentation festgehaltene Spezifikation. Im Abschnitt zu Single-Threaded Apartments wird ausdrücklich festgehalten, dass sich innerhalb desselben Apartments Schnittstellenzeiger ohne Marshalling übergeben lassen, dass beim Überschreiten einer Apartment-Grenze auch innerhalb desselben Prozesses derselbe Marshalling-Mechanismus wie zwischen Prozessen verwendet wird, und dass der Aufruf als Fenster-Nachricht an ein verstecktes Fenster (OleMainThreadWndClass) ankommt. Diese Struktur ist der Grund, warum sich die Größenordnung ändert – die konkreten Zahlen selbst hängen von Umgebung und der Komplexität der Argumente ab.

Wenn Sie das für Ihren eigenen Fall beurteilen möchten, ist es am zuverlässigsten, dieselbe Schnittstelle einmal aus demselben Apartment und einmal aus einem anderen Apartment aufzurufen und die Zeiten zu vergleichen.

// C#. Denselben Aufruf N-mal wiederholen und die Zeit pro Aufruf ermitteln
static void Measure(string label, Action call, int iterations = 100_000)
{
    call(); // Die anfängliche Verzögerung (JIT, Proxy-Erzeugung, Verbindungsaufbau) aus der Messung ausschließen

    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");
}

Misst man sowohl bei einer Methode mit wenigen Argumenten als auch bei einer Methode, die Strings oder Arrays übergibt, zeigt sich zudem, dass „die Menge des zu marshallenden Materials die Auswirkung verändert“.

3. STA (Single-Threaded Apartment)

STA ist das Modell „1 Thread = 1 Apartment“.

  • Die COM-Objekte in diesem Apartment laufen grundsätzlich nur auf diesem Thread
  • Wird von einem anderen Thread aus aufgerufen, leitet COM den Aufruf über die Nachrichtenwarteschlange/RPC weiter
  • Wird häufig auf UI-Threads (WinForms/WPF) verwendet (da die UI ebenfalls „Ein-Thread-Affinität + Nachrichtenschleife“ mitbringt, passt das gut zusammen)

3.1. Warum STA auf UI-Threads verwendet wird

Weil UI-Thread und STA im Design übereinstimmen.

  • UI-Steuerelemente sind nicht threadsicher Schaltflächen, Textfelder und dergleichen lassen sich nur von dem Thread aus sicher bedienen, der sie erzeugt hat
  • Auch STA hat „Ein-Thread-Affinität“ COM-Objekte werden nur auf dem Thread, der sie erzeugt hat, direkt ausgeführt
  • Der UI-Thread betreibt zwangsläufig eine Nachrichtenschleife Notwendig zur Verarbeitung von Fensterereignissen. Das entspricht genau der von STA vorausgesetzten Message Pump

Deshalb ist der UI-Thread bei WinForms/WPF standardmäßig STA.

Wichtig: STA bietet zwar eine hohe Thread-Affinität, neigt dafür aber bei vielen Aufrufern zu Staus.

4. MTA (Multi-Threaded Apartment)

MTA ist das Modell „mehrere Threads teilen sich ein Apartment“.

  • COM-Objekte werden von mehreren Threads gleichzeitig aufgerufen
  • Das Objekt selbst muss threadsicher entworfen sein
  • Geeignet für serverseitige Verarbeitung und Hintergrundverarbeitung

Wichtig: MTA bietet hohe Parallelität, legt aber eine große Verantwortung auf die Implementierung des Objekts.

5. Wo STA/MTA festgelegt wird

Das Apartment eines COM-Threads wird dadurch festgelegt, dass jeder Thread einzeln initialisiert wird.

  • In dem Moment, in dem CoInitialize / CoInitializeEx aufgerufen wird, steht das Apartment dieses Threads fest
  • STA: COINIT_APARTMENTTHREADED
  • MTA: COINIT_MULTITHREADED

5.1. STA/MTA in .NET

Auch .NET besitzt die Attribute [STAThread] / [MTAThread] und ApartmentState, aber das sind Wrapper zum Festlegen des COM-Apartment-Modells.

  • [STAThread] → wird an der Main-Methode (dem Einstiegspunkt) angebracht. Bei Verwendung von COM wird als STA initialisiert
  • [MTAThread] → ebenfalls für die Main-Methode. Initialisiert als MTA
  • Thread.SetApartmentState(ApartmentState.STA)für zusätzlich erzeugte Threads. Muss vor dem Threadstart gesetzt werden

Zu beachten:

  • Auch mit [STAThread] wird erst initialisiert, wenn tatsächlich COM verwendet wird (ohne COM-Nutzung hat es keine Wirkung)
  • Auf zusätzliche Threads wirkt [STAThread] nicht. Dort ist Thread.SetApartmentState zu verwenden

Mit anderen Worten: STA/MTA in .NET sind genau STA/MTA von COM – ein Mechanismus, der für COM Interop bereitgestellt wird.

Wichtig: Ein Apartment lässt sich nachträglich nicht mehr ändern. Die erste Initialisierung entscheidet alles.

6. Ein konkretes Beispiel für einen Hänger durch falsches STA

Ein Aufbau wie der folgende führt in der Praxis leicht zu einem Hänger.

6.1. Eine typische Situation

  • Im Hintergrund wird ein STA-Thread erzeugt und darauf ein COM-Objekt instanziiert
  • Dieser Thread betreibt keine Nachrichtenschleife
  • Ein anderer Thread (egal ob STA oder MTA) ruft dieses COM-Objekt auf

6.2. Was dabei passiert

Der Grund für den Hänger lässt sich auf zwei Voraussetzungen von STA zurückführen. Dieser Abschnitt liefert die Erklärung des Grundes; in den folgenden Abschnitten wird sie nicht wiederholt.

  • Ein COM-Objekt wird auf dem STA-Thread verarbeitet, der es erzeugt hat. Ob der Aufrufer STA oder MTA ist, spielt keine Rolle – ein Aufruf von einem anderen Thread wird immer an diesen STA-Thread weitergeleitet. Die Weiterleitung erfolgt als Fenster-Nachricht an ein verstecktes Fenster (Fensterklasse OleMainThreadWndClass), das COM in diesem Apartment anlegt
  • Um diese Weiterleitung zu empfangen, muss der STA-Thread eine Message Pump betreiben. Auch in der Microsoft-Dokumentation steht ausdrücklich, dass jedes STA eine Nachrichtenschleife besitzen muss, um Aufrufe von anderen Prozessen oder anderen Apartments im selben Prozess zu verarbeiten

Ein STA-Thread, der keine Nachrichten verarbeitet, kann folglich keine Aufrufe empfangen; der Aufrufer wartet weiterhin auf eine Antwort, und im Ergebnis kommt es zu einem Hänger.

Das gilt übrigens auch für .NET. In der Dokumentation zu Single-Threaded Apartments findet sich die Warnung, dass das Blockieren eines STA-Threads mit Task.Wait(), Task.Result, Thread.Sleep(), ManualResetEvent.WaitOne() und Ähnlichem dazu führt, dass COM-Callbacks oder Aufrufe über Apartment-Grenzen hinweg nicht abgeschlossen werden können und es zu einem Deadlock kommt. Dass das Fehlerbeispiel im folgenden Abschnitt 6.3 bei WaitOne() hängen bleibt, ist genau dieser Fall.

Der UI-Thread hingegen betreibt von Anfang an eine Nachrichtenschleife, um Fensterereignisse zu verarbeiten, und erfüllt damit die Anforderungen von STA, ohne dass dafür etwas zusätzlich implementiert werden müsste. Das ist der Grund, warum der UI-Thread der naheliegende Ort ist, um STA-COM-Objekte auszuführen.

6.3. Pseudocode (das typische Fehlermuster)

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(() =>
        {
            // Als STA initialisieren
            CoInitializeEx(IntPtr.Zero, COINIT_APARTMENTTHREADED);

            // Es wird eine COM-Klasse vorausgesetzt, die mit ThreadingModel=Apartment registriert ist
            Type type = Type.GetTypeFromProgID(progId, throwOnError: true);
            comObj = Activator.CreateInstance(type);
            ready.Set();

            // Wartet, ohne dass eine Nachrichtenschleife läuft -> genau das ist der fatale Fehler
            done.WaitOne();

            CoUninitialize();
        });

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

        ready.WaitOne();

        try
        {
            // Ruft man von einem anderen Thread (egal ob STA oder MTA) auf, wird der Aufruf an die STA weitergeleitet
            // Da die STA-Seite aber keine Nachrichten verarbeitet, kommt es hier leicht zu einem Hänger
            dynamic obj = comObj;
            obj.AnyMethod();
        }
        finally
        {
            // Auch wenn der Aufruf zurückkehrt – etwa weil die Klasse agile war,
            // der Aufruf nicht weitergeleitet wurde, oder AnyMethod sofort zurückkehrte –
            // wird der STA-Thread stets freigegeben. Lässt man das weg, bleibt ein
            // Vordergrundthread bestehen, der weiter auf done wartet, sodass der
            // Prozess auch dann nicht endet, wenn sich der Fehler "nicht reproduzieren" ließ.
            // Dadurch lässt sich am Symptom nicht mehr unterscheiden, ob er reproduziert wurde oder nicht
            done.Set();
            staThread.Join();
        }
    }
}

Dieser Code setzt voraus, dass die ProgID einer COM-Klasse übergeben wird, die als ThreadingModel=Apartment (= STA) registriert ist. Ersetzen Sie AnyMethod durch den tatsächlichen Methodennamen dieser Klasse. Nicht bei jeder COM-Klasse lässt sich das reproduzieren. Für die Reproduktion sind die folgenden drei Bedingungen nötig.

Bedingung Prüfmethode
Der ThreadingModel-Wert der Zielklasse ist Apartment Prüfen Sie den Wert ThreadingModel unter HKEY_CLASSES_ROOT\CLSID\{CLSID}\InprocServer32 in der Registry. Bei Both oder Free wird der Aufruf nicht weitergeleitet, und das Verhalten lässt sich nicht reproduzieren
Der STA-Thread verarbeitet keine Nachrichten Im obigen Beispiel entspricht das done.WaitOne()
Der Aufruf erfolgt von einem anderen Thread aus Solange der Aufruf innerhalb desselben STA-Threads erfolgt, handelt es sich um einen direkten Aufruf, und es kommt zu keinem Hänger

Dass obj.AnyMethod() in try/finally eingebettet ist und done.Set() sowie staThread.Join() immer durchlaufen werden, dient dazu, genau diesen „Fall, in dem sich das Problem nicht reproduziert“, zu erkennen. STA-Threads sind standardmäßig Vordergrundthreads, sodass der Prozess so lange nicht endet, bis done gesetzt wird. Lässt man das finally weg, sieht man von außen in beiden Fällen – wenn sich das Problem reproduziert (der Aufruf kehrt nicht zurück) und wenn nicht (der Aufruf kehrt zurück, aber niemand setzt done) – dasselbe Symptom: „Der Prozess endet nicht.“ Das Werkzeug, mit dem man die Reproduktionsbedingung prüfen wollte, verdeckt dann selbst, ob die Bedingung erfüllt war. In der obigen Form endet der Prozess dagegen normal, wenn sich das Problem nicht reproduzieren lässt.

Um festzustellen, dass der Prozess wirklich hängt, ist es am schnellsten, den Prozess im Debugger anzuhalten und zu prüfen, dass der Stack des aufrufenden Threads in einer COM-Wartefunktion steht und der STA-Thread bei WaitOne hängt.

COM-RuntimeSTA-ThreadHauptthreadCOM-RuntimeSTA-ThreadHauptthreadKeine Nachrichtenschleifehier hängt es festLeitet über eine Nachricht weiter, aber...Ist in WaitOne, kannNachrichten nicht verarbeitenDer Aufrufer wartet ebenfalls weiterBeide warten → HängerThread gestartetCoInitializeEx (STA)COM-Objekt erzeugtready.Set()Wartet mit done.WaitOne()CallComObject()Versucht, den Aufruf weiterzuleiten

Die beiden mittleren Zeilen im Diagramm, „Leitet über eine Nachricht weiter, aber …“, sind genau die Stelle, an der die beiden in 6.2 beschriebenen Voraussetzungen zusammenbrechen.

6.4. Die wichtigsten Punkte zur Vermeidung

  • Wenn Aufrufe von einem anderen Thread empfangen werden sollen, muss der STA-Thread eine Nachrichtenschleife betreiben
  • Wenn möglich, auf dem UI-Thread erzeugen und verwenden (der UI-Thread hat von Anfang an eine Nachrichtenschleife)
  • Wenn STA nicht benötigt wird, von vornherein MTA verwenden

Ergänzung: Bleibt alles innerhalb desselben Threads, ist Application.Run() nicht zwangsläufig immer nötig. Da bei UI- und COM-bezogenem Code aber häufig Aufrufe von anderen Threads eine Rolle spielen, ist es in der Praxis fast unverzichtbar.

6.5. Was bedeutet „eine Nachrichtenschleife betreiben“ eigentlich?

Es handelt sich um genau das, was jeder Win32-UI-Thread tut.

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

Bei STA werden Aufrufe von anderen Threads „weitergeleitet“. Diese Schleife (die Message Pump) ist es, die diese Weiterleitung entgegennimmt und zur Ausführung bringt.

6.6. Ein Beispiel in die richtige Richtung (grob skizziert)

Wenn man „COM auf einer Hintergrund-STA verwenden möchte“, sieht das etwa so aus.

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

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

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

    // Solange der STA-Thread lebt, Nachrichten verarbeiten
    Application.Run();

    CoUninitialize();
});

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

ready.WaitOne();
CallComObject(comObj);

(Hinweis: Das Vergessen von CoInitializeEx / CoUninitialize führt ganz gewöhnlich zu Problemen. Die P/Invoke-Deklaration von CoInitializeEx wird dieselbe benötigt wie in 6.3.)

Die parameterlose Form von Application.Run() dient dazu, nur die Nachrichtenschleife zu betreiben, ohne ein Formular zu besitzen. Als Einsatzzweck ist genau dieses Szenario gemeint, aber beachten Sie die folgenden zwei Punkte.

  • Es ist eine Referenz auf System.Windows.Forms erforderlich. Bei einer Konsolenanwendung fügen Sie der Projektdatei <UseWindowsForms>true</UseWindowsForms> hinzu
  • Diese Schleife endet nicht von selbst. Zum Beenden rufen Sie auf diesem Thread Application.ExitThread() (oder Application.Exit(), das die gesamte Anwendung beendet) auf. Soll im obigen Beispiel auch CoUninitialize() erreicht werden, ist zusätzlich ein Mechanismus nötig, der dem STA-Thread ein „Beenden“ mitteilt und ExitThread aufrufen lässt

Wenn Sie nicht von WinForms abhängig sein möchten, schreiben Sie entweder die GetMessage/DispatchMessage-Schleife aus 6.5 selbst, oder verwenden Sie MsgWaitForMultipleObjects, das sowohl auf Nachrichten als auch auf Synchronisationsobjekte gleichzeitig warten kann. Letzteres ist der Standardansatz, wenn Sie „per Event über das Beenden benachrichtigt werden wollen, ohne dabei COM-Aufrufe zu verpassen“.

6.7. Ein weiteres Hänger-Beispiel: Callback während eines synchronen Aufrufs

Bei STA werden nicht nur „Aufrufe weitergeleitet“ – je nach Situation kommen auch Callbacks in die entgegengesetzte Richtung (Server → Client). Insbesondere das Muster, bei dem während eines synchronen Aufrufs ein Callback eintrifft, ist der klassische Fall für einen Deadlock.

COM-ServerUI-Thread (STA)COM-ServerUI-Thread (STA)Wartet auf die Rückkehr von DoWork(verarbeitet keine Nachrichten)Kann den Callback nicht empfangen,da im WartezustandWartet auf den Abschluss des CallbacksBeide warten aufeinander → DeadlockDoWork() (synchroner Aufruf)ProgressCallback() (Callback)

Warum es so leicht zu einem Deadlock kommt:

  1. Der UI-Thread ruft DoWork() synchron (blockierend) auf
  2. Der UI-Thread wartet auf die Rückkehr (er verarbeitet keine Nachrichten)
  3. Der Server sendet ProgressCallback() an den UI-Thread
  4. Der UI-Thread befindet sich im Wartezustand und kann den Callback nicht empfangen
  5. Der Server wartet auf den Abschluss des Callbacks
  6. Beide warten aufeinander → es geht nie weiter

Wie lange die Verarbeitung dauert, spielt keine Rolle. Dass während eines synchronen Aufrufs ein Callback eintrifft ist als Muster selbst das Problem.

Ergänzung: COM verfügt je nach Situation auch über Mechanismen zur Nachrichtenverarbeitung und zum Reentrancy, sodass sich das Verhalten je nach Komponente und Aufrufform unterscheidet. Es kommt nicht zwangsläufig zu einem Deadlock, aber dieses Muster ist sicherheitshalber zu vermeiden.

7. Grobe Entscheidungshilfe

Situation Wahl Begründung Zusätzlich nötig
UI ist beteiligt (WinForms / WPF) STA UI-Steuerelemente haben Ein-Thread-Affinität, und der UI-Thread hat ohnehin eine Nachrichtenschleife (3.1) Nichts Besonderes. Standardmäßig bereits STA
Umfangreiche parallele Verarbeitung gewünscht MTA Mehrere Threads teilen sich ein Apartment und können ohne Weiterleitung direkt aufrufen (2.2) Threadsicheres Design auf der COM-Objektseite. Das ist Verantwortung der Objektimplementierung, nicht des Exklusivzugriffs auf der Aufruferseite
COM im Hintergrund über STA verwenden STA + Nachrichtenschleife Da von einem anderen Thread aus aufgerufen wird, braucht es eine Stelle, die die Weiterleitung entgegennimmt (6.2) Application.Run() oder eine GetMessage-Schleife. Auch ein Mittel zum Beenden (etwa ExitThread) muss mitgeplant werden (6.6)
Die Anforderungen der verwendeten COM-Komponente stehen fest Nach der Gegenseite richten Das Apartment ist die Aufrufregel selbst und lässt sich nachträglich nicht ändern (Kapitel 5) Prüfen Sie den Wert von ThreadingModel. Bei Apartment planen Sie mit STA als Voraussetzung
Häufige Aufrufe Sich dem Apartment des Aufrufers annähern Bei jedem Überschreiten der Grenze wird Marshalling zwischengeschaltet (2.4) Ist das schwierig, reduzieren Sie stattdessen die Anzahl der Aufrufe selbst (z. B. durch gebündelte Übergabe)

Sind Sie unsicher, lässt sich die Entscheidung leichter treffen, wenn Sie in der Reihenfolge „Anforderungen der Gegenseite > Vorhandensein einer UI > Parallelität“ vorgehen. Da das Apartment bei der ersten Initialisierung feststeht und sich später nicht mehr ändern lässt, sollten Sie zumindest diesen Punkt klären, bevor Sie mit der Implementierung beginnen.

8. Zusammenfassung

STA/MTA ist das Threadmodell für COM: STA nimmt die Form „1 Thread = 1 Apartment“ an, MTA die Form „mehrere Threads teilen sich 1 Apartment“. Aufrufe über Apartment-Grenzen hinweg leitet COM über Proxy/Stub weiter (bei Nicht-Standardschnittstellen ist eine Generierung und Registrierung per MIDL o. Ä. nötig), doch das ist mit Marshalling-Overhead verbunden, weshalb das Apartment-Design überall dort sorgfältig festgelegt werden sollte, wo häufige Aufrufe zu erwarten sind.

In Bezug auf Hänger lässt sich alles auf einen Punkt zurückführen: „Ein STA-Thread, der Aufrufe von einem anderen Thread empfängt, setzt voraus, dass er eine Message Pump betreibt.“ Ruft man einen STA-Thread auf, der keine Nachrichten verarbeitet, kommt es leicht zu einem Hänger, und auch das Muster, bei dem während eines synchronen Aufrufs ein Callback eintrifft, führt leicht zu einem Deadlock. Der UI-Thread besitzt von Anfang an sowohl „Ein-Thread-Affinität“ als auch eine „Nachrichtenschleife“ und erfüllt diese Voraussetzung damit, ohne dass zusätzlich etwas implementiert werden müsste – deshalb passt er so gut zu STA-COM.

9. Referenzen

  • 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 (Grund, warum eine Nachrichtenschleife zwingend erforderlich ist, das versteckte Fenster, die .NET-Deadlock-Warnung) 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 (der Wert von ThreadingModel) https://learn.microsoft.com/en-us/windows/win32/com/inprocserver32
  • Application.Run-Methode (die Form, mit der man ohne Formular eine Nachrichtenschleife betreibt, und wie man sie stoppt) https://learn.microsoft.com/en-us/dotnet/api/system.windows.forms.application.run
  • MsgWaitForMultipleObjects (gleichzeitiges Warten auf Nachrichten und Synchronisationsobjekte) https://learn.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-msgwaitformultipleobjects

Word-Datei zu diesem Artikel herunterladen

Aktuelle Artikel mit denselben Schlagwörtern führen zu verwandten Themen weiter.

Diese Seiten ordnen den Artikel in einen größeren Leistungs- und Entscheidungskontext ein.

Dieser Artikel ist direkt mit den folgenden Leistungen verbunden.

Häufige Fragen

Fragen, die in Beratungen zu diesem Artikelthema häufig gestellt werden.

Sollte ich STA oder MTA wählen?
Bei Verarbeitung mit UI-Bezug ist STA die Grundregel, bei umfangreicher paralleler Verarbeitung MTA. STA hat als Modell mit einem Apartment pro Thread eine hohe Thread-Affinität, neigt aber bei vielen Aufrufern zu Staus. MTA teilt sich ein Apartment über mehrere Threads und bietet dadurch hohe Parallelität, verlangt aber vom COM-Objekt selbst ein threadsicheres Design. Ist keines von beiden eindeutig, ist es realistisch, sich nach den Anforderungen der verwendeten bestehenden Bibliotheken oder COM-Server zu richten.
Warum ist der UI-Thread STA?
Weil UI-Thread und STA im Design übereinstimmen. UI-Steuerelemente wie Schaltflächen oder Textfelder sind nicht threadsicher und lassen sich nur von dem Thread aus sicher bedienen, der sie erzeugt hat. STA ist ebenfalls ein Modell mit Ein-Thread-Affinität. Außerdem betreibt der UI-Thread zur Verarbeitung von Fensterereignissen zwangsläufig eine Nachrichtenschleife und erfüllt damit die von STA vorausgesetzte Message Pump, ohne dass dafür etwas zusätzlich implementiert werden müsste. Deshalb ist der UI-Thread bei WinForms/WPF standardmäßig STA.
Warum kommt es beim Aufruf eines STA-COM-Objekts zu einem Hänger?
Aufrufe an ein STA-COM-Objekt werden auf dem STA-Thread verarbeitet, der es erzeugt hat. Aufrufe von einem anderen Thread leitet COM über Nachrichten/RPC weiter, aber wenn der STA-Thread keine Nachrichtenschleife betreibt, kann er diese Weiterleitung nicht empfangen, sodass der Aufrufer wartet und es zum Hänger kommt. Vermeiden lässt sich das, indem der STA-Thread, der von einem anderen Thread aus aufgerufen wird, eine Nachrichtenschleife betreibt, das Objekt auf dem UI-Thread erzeugt und verwendet wird, oder – falls STA nicht benötigt wird – von vornherein MTA verwendet wird.
Wozu dient das [STAThread]-Attribut in .NET?
Es ist ein Wrapper zum Festlegen des COM-Apartment-Modells. Wird es an der Main-Methode angebracht, wird dieser Thread bei Verwendung von COM als STA initialisiert. Initialisiert wird allerdings erst, wenn COM tatsächlich verwendet wird – bei Anwendungen ohne COM-Nutzung hat es keine Wirkung. Außerdem wirkt es nicht auf zusätzlich erzeugte Threads; dafür ist vor dem Threadstart Thread.SetApartmentState zu verwenden. Zu beachten ist außerdem, dass das Apartment bei der ersten Initialisierung festgelegt wird und sich später nicht mehr ändern lässt.

Autorenprofil

Profilseite des Artikelautors.

Go Komura

Geschäftsführer von KomuraSoft LLC

Spezialisiert auf Windows-Softwareentwicklung, technische Beratung und Fehleranalyse, insbesondere bei bestehenden Systemen und schwer reproduzierbaren Störungen.

Zurück zum Blog