Grundlagenwissen zu COM STA/MTA – Threadmodelle und wie man Hänger vermeidet
· Go Komura · 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
- Zuerst das Fazit (in Kürze)
- 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
- STA (Single-Threaded Apartment)
- 3.1. Warum STA auf UI-Threads verwendet wird
- MTA (Multi-Threaded Apartment)
- Wo STA/MTA festgelegt wird
- 5.1. STA/MTA in .NET
- 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
- Grobe Entscheidungshilfe
- Zusammenfassung
- 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.
flowchart LR
subgraph STA[STA-Thread]
Caller[Aufrufender Code]
Obj[COM-Objekt]
Caller -->|Direkter Aufruf| Obj
end
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.
flowchart LR
subgraph MTA[MTA - ein Apartment]
Thread1[Worker-Thread 1]
Thread2[Worker-Thread 2]
Obj[COM-Objekt]
Thread1 -->|Direkter Aufruf| Obj
Thread2 -->|Direkter Aufruf| Obj
end
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.
flowchart LR
subgraph STA[STA-Thread]
StaCaller[Aufrufender Code]
end
subgraph RT[COM-Runtime - automatisch]
Proxy[Proxy]
RPC[RPC/IPC]
Stub[Stub]
Proxy --> RPC --> Stub
end
subgraph MTA[MTA-Thread]
MtaObj[COM-Objekt]
end
StaCaller -->|Aufruf| Proxy
Stub -->|Weiterleitung| MtaObj
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/CoInitializeExaufgerufen 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 MTAThread.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 istThread.SetApartmentStatezu 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.
sequenceDiagram
participant Main as Hauptthread
participant STA as STA-Thread
participant COM as COM-Runtime
Main->>STA: Thread gestartet
STA->>STA: CoInitializeEx (STA)
STA->>STA: COM-Objekt erzeugt
STA->>Main: ready.Set()
STA->>STA: Wartet mit done.WaitOne()
Note over STA: Keine Nachrichtenschleife<br/>hier hängt es fest
Main->>COM: CallComObject()
COM->>STA: Versucht, den Aufruf weiterzuleiten
Note over COM: Leitet über eine Nachricht weiter, aber...
Note over STA: Ist in WaitOne, kann<br/>Nachrichten nicht verarbeiten
Note over Main: Der Aufrufer wartet ebenfalls weiter
Note over Main,STA: Beide warten → Hänger
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.Formserforderlich. 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()(oderApplication.Exit(), das die gesamte Anwendung beendet) auf. Soll im obigen Beispiel auchCoUninitialize()erreicht werden, ist zusätzlich ein Mechanismus nötig, der dem STA-Thread ein „Beenden“ mitteilt undExitThreadaufrufen 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.
sequenceDiagram
participant UI as UI-Thread (STA)
participant Server as COM-Server
UI->>Server: DoWork() (synchroner Aufruf)
Note over UI: Wartet auf die Rückkehr von DoWork<br/>(verarbeitet keine Nachrichten)
Server->>UI: ProgressCallback() (Callback)
Note over UI: Kann den Callback nicht empfangen,<br/>da im Wartezustand
Note over Server: Wartet auf den Abschluss des Callbacks
Note over UI,Server: Beide warten aufeinander → Deadlock
Warum es so leicht zu einem Deadlock kommt:
- Der UI-Thread ruft
DoWork()synchron (blockierend) auf - Der UI-Thread wartet auf die Rückkehr (er verarbeitet keine Nachrichten)
- Der Server sendet
ProgressCallback()an den UI-Thread - Der UI-Thread befindet sich im Wartezustand und kann den Callback nicht empfangen
- Der Server wartet auf den Abschluss des Callbacks
- 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
Verwandte Artikel
Aktuelle Artikel mit denselben Schlagwörtern führen zu verwandten Themen weiter.
Laufen Business-Anwendungen unter Windows on Arm? ── Die Realität von x64-Emulation (Prism) und nativen DLLs/COM
Eine Antwort für Entwicklerinnen, Entwickler und IT-Verantwortliche auf die Frage „Läuft unsere Business-Anwendung unter Windows on Arm?“...
Windows-App-Outsourcing und Auftragsentwicklung: Was Sie vor der Beauftragung klären sollten
Bevor Sie die Entwicklung einer Windows-App outsourcen oder in Auftrag geben, sollten Sie folgende Punkte klären: Überarbeitung bestehend...
Die seltsame Liebe eines Entwicklers, oder: Wie ich lernte, mir keine Sorgen mehr zu machen, und Windows lieben lernte
Windows ist umständlich. Aber diese Umständlichkeit ist auch die Umständlichkeit eines Betriebssystems, das jahrzehntelang reale Geschäft...
Registrierungs- und Bitness-Fallen bei der COM/OCX/ActiveX-Entwicklung
Wir ordnen die typischen Fallen der COM-, OCX- und ActiveX-Entwicklung – 32bit/64bit, Visual Studio 2022, regsvr32/Regasm, Administratorr...
Was ist Reg-Free COM - COM ohne Registrierung nutzen
Wir ordnen die Grundlagen von Reg-Free COM, die Rolle von Aktivierungskontext und Manifesten, die Vorteile, die Grenzen und die Entscheid...
Verwandte Themen
Diese Seiten ordnen den Artikel in einen größeren Leistungs- und Entscheidungskontext ein.
Technische Windows-Themen
Portal zu Windows-Entwicklung, Fehleranalyse und der Nutzung bestehender Assets.
ActiveX-Migration
Entscheidungen zum Beibehalten, Kapseln oder Ersetzen von COM / ActiveX / OCX.
Leistungen zu diesem Thema
Dieser Artikel ist direkt mit den folgenden Leistungen verbunden.
Technische Beratung und Design-Review
Die Klärung von STA/MTA, Nachrichtenschleifen und Marshalling ist direkt relevant für die Aufteilung der Zuständigkeiten vor der Implementierung und für Reviews von Thread-Grenzen.
Nutzung und Migration bestehender Assets
Diese Grundlagen lassen sich beim Umgang mit bestehenden Altbeständen, die COM enthalten, kaum umgehen, weshalb sie sich auch gut mit einer Beratung zur Weiternutzung und Migration von Altbeständen kombinieren lassen.
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.