Les fondamentaux STA/MTA de COM - modèles de threads et comment éviter les blocages
· Mis à jour le: · Go Komura · COM, Développement Windows, STA, MTA, Thread
Le couple STA/MTA de COM est un socle de connaissances difficile à éviter dès que l’on fait du développement Windows ou que l’on touche à COM depuis .NET. Les questions les plus fréquentes en recherche sont : pourquoi le thread UI est-il en STA, que se passe-t-il quand un appel franchit un Apartment, et pourquoi cela se bloque-t-il parfois ?
Sommaire
- D’abord, la conclusion (en une ligne)
- Modèles d’appel de l’Apartment Model (schémas)
- STA (Single-Threaded Apartment)
- MTA (Multi-Threaded Apartment)
- Où se décide STA/MTA
- Un exemple concret de blocage causé par une mauvaise utilisation de STA
- 6.1. La situation typique
- 6.2. Ce qui se passe
- 6.3. Pseudocode (le schéma d’échec classique)
- 6.4. Points clés pour l’éviter
- 6.5. Que signifie vraiment « faire tourner la boucle de messages » ?
- 6.6. Un exemple dans la bonne direction (esquissé rapidement)
- 6.7. Un autre exemple de blocage : un callback pendant un appel synchrone
- Guide rapide de choix
- Conclusion
- Références
Dès que l’on utilise COM, la question de « sur quel thread cela s’exécute » est incontournable. Au centre de cette question se trouve le modèle Apartment (STA/MTA). STA/MTA n’est pas un concept général de thread sous Windows : c’est un modèle de threads qui détermine les règles d’appel des objets COM.
Dans cet article, nous expliquons la relation entre STA, MTA et COM à l’aide de schémas, jusqu’à faire le lien avec « pourquoi cela peut bloquer ».
1. D’abord, la conclusion (en une ligne)
- Les règles d’appel d’un objet COM sont déterminées par l’Apartment auquel il appartient
- Il est plus simple de comprendre STA comme 1 Apartment par thread, et MTA comme 1 Apartment partagé par plusieurs threads
- Un appel qui franchit un Apartment est marshalé par COM via un Proxy/Stub
2. Modèles d’appel de l’Apartment Model (schémas)
Il existe globalement trois modèles pour appeler un objet COM.
2.1. Modèle 1 : appel au sein d’un même thread STA
Au sein d’un même thread STA, l’appel est direct. Aucune surcharge.
flowchart LR
subgraph STA[Thread STA]
Caller[Code appelant]
Obj[Objet COM]
Caller -->|Appel direct| Obj
end
2.2. Modèle 2 : appel au sein d’un même MTA
Depuis plusieurs threads d’un même MTA, n’importe quel thread peut appeler directement. Mais l’objet doit alors être impérativement conçu comme thread-safe.
flowchart LR
subgraph MTA["MTA (un seul Apartment)"]
Thread1[Thread de travail 1]
Thread2[Thread de travail 2]
Obj[Objet COM]
Thread1 -->|Appel direct| Obj
Thread2 -->|Appel direct| Obj
end
2.3. Modèle 3 : appel qui traverse les Apartments
Entre des Apartments différents, COM transfère l’appel via un Proxy/Stub. Pour les interfaces standard, le runtime COM s’en charge.
Remarque : les Proxy/Stub ne sont pas automatiquement disponibles pour tout, mais en pratique il est rare d’avoir besoin de les générer explicitement.
| Modèle | Préparation du Proxy/Stub |
|---|---|
Basé sur IDispatch (Automation) |
Inutile. Pris en charge par oleaut32.dll |
| Bibliothèque de types enregistrée | Inutile. Pris en charge par le marshaler de bibliothèque de types |
| .NET COM Interop | Généralement inutile. Fonctionne via la bibliothèque de types |
Interface personnalisée dérivant directement d’IUnknown |
Génération et enregistrement du Proxy/Stub via MIDL requis |
Autrement dit, la génération de Proxy/Stub via MIDL n’est nécessaire que lorsque vous créez une interface qui dérive directement d’IUnknown sans utiliser IDispatch.
Pour les composants COM classiques utilisés depuis .NET ou des langages de script, ce travail est rarement nécessaire.
flowchart LR
subgraph STA[Thread STA]
StaCaller[Code appelant]
end
subgraph RT["Runtime COM (automatique)"]
Proxy[Proxy]
RPC[RPC/IPC]
Stub[Stub]
Proxy --> RPC --> Stub
end
subgraph MTA[Thread MTA]
MtaObj[Objet COM]
end
StaCaller -->|Appel| Proxy
Stub -->|Transfert| MtaObj
Point clé : Franchir un Apartment entraîne une surcharge de marshaling. Pour des appels à haute fréquence, cela affecte les performances, ce qui doit être pris en compte dès la conception.
2.4. Ordre de grandeur de la surcharge du marshaling
Voici des ordres de grandeur généraux (ce ne sont pas des valeurs mesurées ; elles varient fortement selon la situation et la complexité des paramètres).
| Modèle d’appel | Temps indicatif | Ressenti relatif |
|---|---|---|
| Même Apartment (direct) | 10 à 100 nanosecondes | Quasiment comme un appel de fonction normal |
| Apartments différents (même processus) | 1 à 10 microsecondes | 100 à 1000 fois un appel direct |
| Processus différents (Out-of-proc) | 100 à 1000 microsecondes | 10 000 à 100 000 fois un appel direct |
Comparaison relative :
- Même Apartment : à peu près un accès mémoire
- Apartments différents : à peu près un appel système
- Processus différents : à peu près une communication réseau vers localhost
Dans un scénario où l’on appelle 10 000 fois en boucle, cet écart devient très sensible.
3. STA (Single-Threaded Apartment)
STA est le modèle « 1 thread = 1 Apartment ».
- Les objets COM de cet Apartment s’exécutent fondamentalement uniquement sur ce thread
- Un appel depuis un autre thread est transféré par COM via la file de messages/RPC
- Couramment utilisé sur le thread UI (WinForms/WPF) - l’UI a elle aussi une « affinité à un seul thread + une boucle de messages », donc l’adéquation est naturelle
3.1. Pourquoi STA est utilisé sur les threads UI
Parce que la conception du thread UI et celle de STA coïncident.
- Les contrôles UI ne sont pas thread-safe Les boutons, zones de texte, etc. ne peuvent être manipulés en toute sécurité que depuis le thread qui les a créés
- STA a lui aussi une « affinité à un seul thread » Un objet COM ne s’exécute directement que sur le thread qui l’a créé
- Le thread UI fait toujours tourner une boucle de messages C’est indispensable pour traiter les événements de fenêtre, ce qui coïncide avec le prérequis de STA (une pompe de messages)
C’est pourquoi le thread UI de WinForms/WPF est en STA par défaut.
Point clé : STA offre une forte affinité de thread, mais en contrepartie il a tendance à se congestionner quand les appelants sont nombreux.
4. MTA (Multi-Threaded Apartment)
MTA est le modèle « plusieurs threads pour 1 Apartment ».
- Un objet COM est appelé simultanément depuis plusieurs threads
- Une conception thread-safe est obligatoire côté objet
- Adapté au traitement côté serveur ou en arrière-plan
Point clé : MTA offre un haut degré de parallélisme, mais fait peser une lourde responsabilité sur l’implémentation de l’objet.
5. Où se décide STA/MTA
L’Apartment COM se décide en initialisant chaque thread.
- Au moment où vous appelez
CoInitialize/CoInitializeEx, l’Apartment de ce thread est fixé - STA :
COINIT_APARTMENTTHREADED - MTA :
COINIT_MULTITHREADED
5.1. STA/MTA en .NET
.NET possède aussi les attributs [STAThread] / [MTAThread] et ApartmentState, mais ce sont des wrappers qui configurent l’Apartment Model de COM.
[STAThread]→ s’applique à la méthode Main (le point d’entrée). Le thread est initialisé en STA au moment où COM est utilisé[MTAThread]→ également pour la méthode Main. Initialisé en MTAThread.SetApartmentState(ApartmentState.STA)→ pour les threads supplémentaires que vous créez. Doit être défini avant le démarrage du thread
Points d’attention :
- Même avec
[STAThread], rien n’est initialisé tant que COM n’est pas réellement appelé (aucun effet si vous n’utilisez pas COM) [STAThread]n’a aucun effet sur les threads supplémentaires. UtilisezThread.SetApartmentState
Autrement dit, le STA/MTA de .NET est le STA/MTA de COM lui-même - un mécanisme prévu pour COM Interop.
Important : Vous ne pouvez pas changer d’Apartment après coup. La première initialisation est définitive.
6. Un exemple concret de blocage causé par une mauvaise utilisation de STA
Une configuration comme celle-ci provoque réellement des blocages assez facilement.
6.1. La situation typique
- Un thread STA est créé en arrière-plan et un objet COM y est instancié
- Ce thread ne fait pas tourner de boucle de messages
- Un autre thread (STA ou MTA, peu importe) appelle cet objet COM
6.2. Ce qui se passe
Les appels vers un objet COM en STA sont traités sur ce thread STA. Que l’appelant soit en STA ou en MTA, s’il s’agit d’un thread différent, COM transfère l’appel via des messages/RPC. Mais si le thread STA ne traite pas les messages, l’appel attend indéfiniment, et le résultat est un blocage.
6.3. Pseudocode (le schéma d’échec classique)
var ready = new AutoResetEvent(false);
var done = new AutoResetEvent(false);
object comObj = null;
var staThread = new Thread(() =>
{
// Initialise en tant que STA
CoInitializeEx(IntPtr.Zero, COINIT_APARTMENTTHREADED);
comObj = new SomeStaComObject();
ready.Set();
// Attente sans boucle de messages -> c'est le point fatal
done.WaitOne();
});
staThread.SetApartmentState(ApartmentState.STA);
staThread.Start();
ready.WaitOne();
// Un appel depuis un autre thread (STA ou MTA) est transféré vers le STA
// Mais le côté STA ne traite pas les messages, donc cela a tendance à bloquer ici
CallComObject(comObj);
sequenceDiagram
participant Main as Thread principal
participant STA as Thread STA
participant COM as Runtime COM
Main->>STA: Démarrage du thread
STA->>STA: CoInitializeEx (STA)
STA->>STA: Création de l'objet COM
STA->>Main: ready.Set()
STA->>STA: Attente sur done.WaitOne()
Note over STA: Pas de boucle de messages<br/>Bloqué ici
Main->>COM: CallComObject()
COM->>STA: Tente de transférer l'appel
Note over COM: Transfert via un message, mais...
Note over STA: Bloqué dans WaitOne, donc<br/>ne peut pas traiter les messages
Note over Main: L'appelant continue lui aussi d'attendre
Note over Main,STA: Les deux attendent → blocage
En résumé, la cause du blocage tient à deux prérequis de STA.
- Un objet COM est traité sur le thread STA qui l’a créé Les appels depuis un autre thread sont toujours transférés vers ce thread STA
- Pour recevoir ce transfert, le thread STA doit faire tourner une pompe de messages S’il ne la fait pas tourner, il ne peut pas recevoir l’appel
Donc :
- Un thread STA qui ne fait pas tourner de messages ne peut pas recevoir d’appels
- Comme il ne peut pas les recevoir, l’appelant continue d’attendre, et le résultat est un blocage
Le thread UI, à l’inverse, fait tourner une boucle de messages dès le départ pour traiter les événements de fenêtre, et satisfait donc les exigences de STA sans implémentation supplémentaire. C’est pour cette raison que le thread UI est l’endroit naturel pour faire vivre des objets COM en STA.
6.4. Points clés pour l’éviter
- Si le thread STA doit recevoir des appels depuis un autre thread, il doit faire tourner une boucle de messages
- Si possible, créez et utilisez l’objet sur le thread UI (qui a une boucle de messages dès le départ)
- Si STA n’est pas nécessaire, passez directement en MTA
Remarque : si tout reste au sein du même thread, Application.Run() n’est pas toujours obligatoire.
Cependant, le code lié à l’UI ou à COM implique très souvent des appels depuis un autre thread, ce qui le rend en pratique quasiment indispensable.
6.5. Que signifie vraiment « faire tourner la boucle de messages » ?
C’est ce schéma familier que tout thread UI Win32 exécute.
while (GetMessage(out var msg, IntPtr.Zero, 0, 0))
{
TranslateMessage(ref msg);
DispatchMessage(ref msg);
}
En STA, les appels provenant d’un autre thread arrivent comme un travail « transféré ». Cette boucle (la pompe de messages) est ce qui reçoit ce travail transféré et le distribue pour exécution.
6.6. Un exemple dans la bonne direction (esquissé rapidement)
Si vous voulez « utiliser COM sur un STA en arrière-plan », voici à quoi cela ressemble.
var ready = new AutoResetEvent(false);
object comObj = null;
var staThread = new Thread(() =>
{
CoInitializeEx(IntPtr.Zero, COINIT_APARTMENTTHREADED);
comObj = new SomeStaComObject();
ready.Set();
// Fait tourner les messages tant que le thread STA est vivant
Application.Run();
CoUninitialize();
});
staThread.SetApartmentState(ApartmentState.STA);
staThread.Start();
ready.WaitOne();
CallComObject(comObj);
(Remarque : oublier d’appeler CoInitializeEx / CoUninitialize est une source d’accidents bien réelle.)
6.7. Un autre exemple de blocage : un callback pendant un appel synchrone
STA ne se limite pas au « transfert des appels » - selon la situation, des callbacks arrivent aussi dans l’autre sens (serveur → client). Parmi eux, le schéma où un callback survient pendant un appel synchrone est le grand classique de l’interblocage.
sequenceDiagram
participant UI as Thread UI (STA)
participant Server as Serveur COM
UI->>Server: DoWork() (appel synchrone)
Note over UI: Attend le retour de DoWork<br/>(ne traite pas les messages)
Server->>UI: ProgressCallback() (callback)
Note over UI: En attente, donc<br/>ne peut pas recevoir le callback
Note over Server: Attend la fin du callback
Note over UI,Server: Chacun attend l'autre → interblocage
Pourquoi cela finit-il si facilement en interblocage :
- Le thread UI effectue un appel synchrone (bloquant) à
DoWork() - Le thread UI attend le retour (il ne traite pas les messages)
- Le serveur envoie
ProgressCallback()au thread UI - Le thread UI est en attente, donc il ne peut pas recevoir le callback
- Le serveur attend la fin du callback
- Chacun attend l’autre → plus rien n’avance
La durée du traitement n’a aucune importance. C’est le schéma lui-même - un callback qui arrive pendant un appel synchrone - qui pose problème.
Remarque : COM dispose aussi, selon les situations, de mécanismes qui font tourner les messages ou autorisent la réentrance, et le comportement varie selon le composant et le mode d’appel. Cela ne finit pas toujours en interblocage, mais mieux vaut éviter ce schéma.
7. Guide rapide de choix
- L’UI est concernée → STA
- Traitement fortement parallèle → MTA
- Ni l’un ni l’autre → alignez-vous sur les exigences de vos bibliothèques existantes ou de votre serveur COM
8. Conclusion
STA/MTA est le modèle de threads propre à COM : STA prend la forme d’1 thread = 1 Apartment, et MTA place plusieurs threads dans 1 Apartment. Les appels qui franchissent un Apartment sont transférés par COM via des Proxy/Stub (les interfaces non standard nécessitent une génération et un enregistrement via MIDL, entre autres), mais cela s’accompagne d’une surcharge de marshaling ; la conception de l’Apartment mérite donc d’être réfléchie avec soin partout où des appels à haute fréquence sont attendus.
Du point de vue des blocages, tout se résume à un seul point : « un thread STA qui reçoit des appels depuis un autre thread est censé faire tourner une pompe de messages ». Appeler un thread STA qui ne traite pas les messages a tendance à bloquer, et le schéma où un callback arrive pendant un appel synchrone finit facilement en interblocage. Le thread UI possède dès le départ à la fois « l’affinité à un seul thread » et « la boucle de messages », ce qui satisfait ces prérequis sans implémentation supplémentaire - c’est précisément pour cela qu’il s’entend si bien avec le COM en STA.
9. Références
- 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
Articles associés
Articles récents partageant les mêmes étiquettes, pour approfondir des sujets proches.
Les applications métier fonctionnent-elles sous Windows on Arm ? — La réalité de l'émulation x64 (Prism) et des DLL/COM natifs
Une réponse, destinée aux développeurs et aux services informatiques, à la question « notre application métier fonctionnera-t-elle sous W...
Externalisation et développement sur mesure d'une application Windows : ce qu'il faut clarifier avant de se lancer
Avant de confier l'externalisation ou le développement sur mesure d'une application Windows, voici les points à clarifier : révision d'un...
L'étrange amour d'un développeur, ou : comment j'ai appris à ne plus m'en faire et à aimer Windows
Windows est contraignant. Mais cette contrainte est aussi celle d'un système d'exploitation qui porte depuis des décennies le poids d'act...
Pièges d'enregistrement et de bitness dans le développement COM/OCX/ActiveX
COM, OCX et ActiveX : nous passons en revue, d'un point de vue pratique, les pièges liés au 32 bits/64 bits, à Visual Studio 2022, à regs...
Qu'est-ce que Reg-Free COM - Utiliser COM sans enregistrement
Un panorama des bases de Reg-Free COM, du rôle des contextes d'activation et des manifestes, de ses avantages, de ses limites, et des cri...
Sujets associés
Ces pages replacent le sujet dans un contexte plus large de services et de décisions.
Thèmes techniques Windows
Portail des sujets sur le développement Windows, l'analyse des incidents et la valorisation des actifs existants.
Migration ActiveX
Choisir de conserver, encapsuler ou remplacer des composants COM / ActiveX / OCX.
Services liés à ce sujet
Cet article est directement lié aux services suivants.
Conseil technique et revue de conception
Clarifier STA/MTA, les boucles de messages et le marshaling est directement lié au découpage des responsabilités avant implémentation et aux revues de frontières entre threads.
Réutilisation et migration d'actifs existants
Ce sont des fondamentaux difficiles à éviter lorsqu'on traite des actifs existants impliquant COM, ce qui s'accorde donc bien avec notre accompagnement en réutilisation et migration d'actifs existants.
Questions fréquentes
Questions souvent posées lors d’une consultation sur le sujet de cet article.
- Faut-il choisir STA ou MTA ?
- La règle de base est : STA pour un traitement impliquant l'UI, MTA pour un traitement fortement parallèle. STA prend la forme d'un Apartment par thread, ce qui lui donne une forte affinité avec le thread, mais il a tendance à devenir congestionné quand les appelants sont nombreux. MTA partage un seul Apartment entre plusieurs threads, ce qui offre un haut degré de parallélisme, mais impose alors une conception thread-safe obligatoire côté objet COM. Si ni l'un ni l'autre ne s'impose clairement, il est réaliste de s'aligner sur les exigences de la bibliothèque existante ou du serveur COM que vous utilisez.
- Pourquoi le thread UI est-il en STA ?
- Parce que la conception du thread UI et celle de STA coïncident. Les contrôles UI comme les boutons ou les zones de texte ne sont pas thread-safe et ne peuvent être manipulés en toute sécurité que depuis le thread qui les a créés. STA est, de la même façon, un modèle à affinité avec un seul thread. De plus, le thread UI fait toujours tourner une boucle de messages pour traiter les événements de fenêtre, ce qui satisfait sans implémentation supplémentaire la pompe de messages exigée par STA. C'est pourquoi le thread UI de WinForms/WPF est en STA par défaut.
- Pourquoi un appel à un objet COM en STA provoque-t-il un blocage ?
- Un appel vers un objet COM en STA est traité sur le thread STA qui l'a créé. Un appel provenant d'un autre thread est transféré par COM via un message/RPC, mais si le thread STA ne fait pas tourner de boucle de messages, il ne peut pas recevoir ce transfert, et l'appelant continue d'attendre, ce qui produit un blocage. Pour l'éviter, il faut faire tourner une boucle de messages sur tout thread STA appelé depuis un autre thread, créer et utiliser l'objet sur le thread UI, ou passer directement en MTA si STA n'est pas nécessaire.
- À quoi sert l'attribut [STAThread] en .NET ?
- C'est un wrapper qui configure l'Apartment Model de COM. Appliqué à la méthode Main, il fait que ce thread est initialisé en STA au moment où COM est utilisé. Cependant, l'initialisation n'a lieu que lorsque COM est réellement appelé, donc cela n'a aucun effet dans une application qui n'utilise pas COM. Il n'a par ailleurs aucun effet sur les threads créés en plus, pour lesquels il faut donc configurer Thread.SetApartmentState avant le démarrage du thread. Il faut aussi noter que l'Apartment est fixé dès la première initialisation et ne peut plus être modifié ensuite.
Profil de l’auteur
Page de présentation de l’auteur de l’article.
Go Komura
Représentant de KomuraSoft LLC
Spécialisé dans le développement de logiciels Windows, le conseil technique et l’analyse de pannes, notamment pour les systèmes existants et les incidents difficiles à reproduire.
Liens publics