Les fondamentaux STA/MTA de COM - modèles de threads et comment éviter les blocages

· Mis à jour le: · · 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

  1. D’abord, la conclusion (en une ligne)
  2. Modèles d’appel de l’Apartment Model (schémas)
  3. STA (Single-Threaded Apartment)
  4. MTA (Multi-Threaded Apartment)
  5. Où se décide STA/MTA
  6. Un exemple concret de blocage causé par une mauvaise utilisation de STA
  7. Guide rapide de choix
  8. Conclusion
  9. 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.

Thread STAAppel directCode appelantObjet COM

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.

MTA (un seul Apartment)Appel directAppel directThread de travail 1Objet COMThread de travail 2

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.

Thread MTARuntime COM (automatique)Thread STAAppelTransfertObjet COMProxyRPC/IPCStubCode appelant

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 MTA
  • Thread.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. Utilisez Thread.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);
Runtime COMThread STAThread principalRuntime COMThread STAThread principalPas de boucle de messagesBloqué iciTransfert via un message, mais...Bloqué dans WaitOne, doncne peut pas traiter les messagesL'appelant continue lui aussi d'attendreLes deux attendent → blocageDémarrage du threadCoInitializeEx (STA)Création de l'objet COMready.Set()Attente sur done.WaitOne()CallComObject()Tente de transférer l'appel

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.

Serveur COMThread UI (STA)Serveur COMThread UI (STA)Attend le retour de DoWork(ne traite pas les messages)En attente, doncne peut pas recevoir le callbackAttend la fin du callbackChacun attend l'autre → interblocageDoWork() (appel synchrone)ProgressCallback() (callback)

Pourquoi cela finit-il si facilement en interblocage :

  1. Le thread UI effectue un appel synchrone (bloquant) à DoWork()
  2. Le thread UI attend le retour (il ne traite pas les messages)
  3. Le serveur envoie ProgressCallback() au thread UI
  4. Le thread UI est en attente, donc il ne peut pas recevoir le callback
  5. Le serveur attend la fin du callback
  6. 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

Télécharger le fichier Word de cet article

Articles récents partageant les mêmes étiquettes, pour approfondir des sujets proches.

Ces pages replacent le sujet dans un contexte plus large de services et de décisions.

Cet article est directement lié aux services suivants.

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.

Retour au blog