Inside COM+ Base Services, Microsoft Press.
Traduction personnelle. But pédagogique.


Modèles de Threading pour Composants In-Process

Les composants in-proc fonctionnement différemment des composants exécutable; ils n'appellent pas CoInitializeEx au démarrage car leur thread client  a déjà été initialisé COM+ au moment ou ils sont chargés. A la place, les composants in-proc déclarent leur modèle de threading en utilisant les paramètres de la base de registres. Dans la clé CLSID\InprocServer32 du composant, la valeur nommée ThreadingModel peut être positionnée à Apartment, Neutral, Free, ou Both pour indiquer le support de différents modèles de threading; la table ci-dessous explique ces valeurs en terme de STA, NA, et MTA.

Valeurs de ThreadingModel Description
Aucune Ancien composant qui tourne seulement dans le STA principal
Apartment STA
Neutral NA
Free MTA
Both Supporte les modèles STA, NA, et MTA

Si aucune valeur ThreadingModel n'est spécifiée, le composant est considéré comme un ancien composant monothread. Différentes coclasses fourni par un même composant peuvent avoir différentes valeurs ThreadingModel—cette valeur est positionnée par CLSID, non par DLL. Vous pourriez écrire une fonction qui enregistre la totalité des valeurs dans la registry pour un composant donné; par exemple RegisterServer:

RegisterServer("component.dll", CLSID_InsideCOM, 
    "Inside COM+ Component", "Component.InsideCOM", 
    "Component.InsideCOM.1", "Apartment");

Vous pouvez aussi écrire une autre fonction, RegisterServerEx, qui fonctionne en fonction d'une table de valeurs:

const REG_DATA g_regData[] = {
    { "CLSID\\{10000002-0000-0000-0000-000000000001}", 0, 
        "Inside COM+ Component" },
    { "CLSID\\{10000002-0000-0000-0000-000000000001}\\"
        "InprocServer32", 0, (const char*)-1 }, 
    { "CLSID\\{10000002-0000-0000-0000-000000000001}\\"
        "InprocServer32", "ThreadingModel", "Apartment" },  
    { "CLSID\\{10000002-0000-0000-0000-000000000001}\\ProgID", 
        0, "Component.InsideCOM.1" }, 
    { "CLSID\\{10000002-0000-0000-0000-000000000001}\\"
        "VersionIndependentProgID", 0, 
        "Component.InsideCOM" },
    { "Component.InsideCOM", 0, 
        "Inside COM+ Component" },
    { "Component.InsideCOM\\CLSID", 0, 
        "{10000002-0000-0000-0000-000000000001}" },
    { "Component.InsideCOM\\CurVer", 0, 
        "Component.InsideCOM.1" },
    { "Component.InsideCOM.1", 0, 
        "Inside COM+ Component" },
    { "Component.InsideCOM.1\\CLSID", 0, 
        "{10000002-0000-0000-0000-000000000001}" },
    { 0, 0, 0 }
};

Interactions entre Appartements

Rappelez vous les principes de base selon lesquels les composants créés leur appartement et déclarent leur modèle de thread supporté:

L'interaction des clients et des composants est différente suivant que les deux parties utilisent le même modèle de threading. Quand un client instancie un objet, COM+ compare les modèles de threading supportés par le client et l'objet. Dans le cas ou les deux parties utilisent le même modèle, COM+ autorise les appels direct du client vers l'objet; c'est la meilleur performance pour un modèle donné. Cependant, les composants exécutable et les composants in-proc qui s'exécutent surrogate utilisent le marshaling pour les appels hors processus ou distant. Si les deux parties ne supportent pas le même modèle de threading, COM+ s'interpose entre le client et l'objet—même si tout fonctionne dans le même processus—pour assurer que les règles sur les threads de chaque partie ne soient pas violées. COM+ réalise cela avec la paire proxy/stub: Le client fait un appel via un proxy, qui utilise COM+ pour délivrer les appels de méthodes vers le stub; le stub appelle l'objet. Dans ce processus de marshaling, un changement de thread (switch) est réalisé entre le thread de l'appelant  et le thread de l'appartement de l'objet.

Imaginez que le thread client qui tourne dans un MTA appelle un composant in-proc supportant le modèle STA. COM+ ne peut pas permettre au client d'appeler directement l'objet in-proc du STA car des accès concurrents peuvent se produire. Quand le pointeur d'interface de l'objet est marshallé en retour au client, COM+ charge la paire proxy/stub et fournit au client un pointeur vers le proxy. Pour cette raison, même les composants in-proc doivent fournir le code de marshaling (sous forme de DLL proxy/stub) pour les interfaces custom qu'ils implémentent si ils sont susceptibles d'être utilisés par différents types d'appartements.

Les objets in-proc qui n'ont pas de valeur ThreadingModel sont considérés comme des anciens composants. Ces composant supposent que le client n'a qu'un seul thread d'exécution, à partir duquel l'objet est crée et utilisé. Pour permettre l'utilisation de ces composants, COM+ créé ces objets dans le STA principal du processus client, quel que soit le thread qui a instancié la coclasse. Un STA principal est le premier STA créé dans un processus. Les appels faits depuis un autre appartement, qu'ils soient de type STA, NA, ou MTA, sont marshalés vers le thread appartenant au STA principal. De tels appels sont reçus par le proxy d'un appartement et sont envoyés au stub dans le STA principal via un marshalling inter-appartement avant d'être délivrés à l'objet.

Comparé aux appels directs, le marshalling inter-appartement est lent; donc vous pourriez réécrire vos anciens composants pour supporter le modèle STA. Une autre solution est d'accéder aux anciens composants seulement depuis le STA principal. Le thread du STA principal peut accéder aux anciens composants directement. La Figure 4-5 montre un ancien composant appelé par deux threads STA—un dans le STA principal, pour accéder directement à l'objet et un autre thread STA qui doit accéder à l'objet via un proxy.

Figure 4-5. Deux threads STA qui accèdent à un objet in-proc qui n'a pas de valeur ThreadingModel spécifiée.

Si vous n'avez pas d'autre alternative que d'utiliser des anciens composants, vous devriez créer le STA principal dans le thread principal de processus client car une fois que le STA principal de termine, tous ses objets in-process sont détruits. Si le client lance plusieurs threads, et que chacun d'eux appele CoInitializeEx avec le flag COINIT_APARTMENTTHREADED, sans avoir créé explicitement le STA principal, un de ces threads deviendra aléatoirement le STA principal dans lequel tous les appels seront marshalés.

Le danger est que le thread du STA principal se termine, et qu'il emporte avec lui tous les objets créés. Une autre difficulté survient lorsqu'un client MTA qui n'a pas de STA instancie une coclasse d'un composant STA. COM+ créé alors automatiquement le STA principal en lançant un nouveau thread et en appelant CoInitializeEx(NULL, COINIT_APARTMENTTHREADED) dans ce thread. L'objet est alors créé dans le nouveau thread STA, et le pointeur d'interface est marshalé au thread du client MTA.

Les Objets qui Supportent le Modèle MTA

Les objets in-proc qui supportent le modèle MTA (ThreadingModel = Free) implémentent leur propre mécanisme de synchronisation et sont conçu pour être utilisé seulement avec le modèle MTA. Les threads des clients MTA qui instancient un objet peuvent y accéder directement; cela apporte une rapidité d'accès supérieure au modèle STA. Initialement, il semble que n'importe objet MTA peut s'exécuter dans un STA. Après tout, l'idée est de protéger les objets STA des accès concurrents, et cela ne concerne pas les objets MTA. Malheureusement, les choses sont plus compliquées. Imaginez un thread client STA qui instancie un objet MTA. Bien que l'objet MTA possède son propre mécanisme de synchronisation, le système doit quand même effectuer des opérations de synchronisation de threads.

Cette fois ci, ce n'est pas le composant mais le client qui a besoin de protection. Le composant a marqué son indépendance via à vis des modèles de threading, le client ne l'a pas fait. Un composant MTA déclare, "Vous pouvez m'appeler depuis n'importe quel thread, je peux vous répondre de n'importe quel autre thread". Le client ne fonctionne pas comme cela. La possibilité de plusieurs accès concurrents dans un STA viole les règles sur les STAs. Pour cette raison, même pour un client STA qui fait un appel vers un objet MTA, COM+ créé l'objet dans le MTA et réalise le marshalling de tous les appels.

La Figure 4-6 montre comment un composant in-proc MTA est créé dans le MTA même si il est créé par un thread STA. Le résultat est que le thread MTA peut appeler directement l'objet, tandis que le thread STA réalise des appels via un proxy. COM+ peut créer l'objet dans un MTA si le client instancie l'objet depuis un STA. Si aucun MTA existe, COM+ en le créé via CoInitializeEx(NULL, COINIT_MULTITHREADED). Cependant, if me MTA existe dans le processus client lorsque l'objet MTA est instancié, COM+ créé l'objet dedans. Tous les appels de et vers l'objet sont marshallé du MTA vers le STA.

Figure 4-6. Quand un thread STA instancie un objet MTA, COM+ créé le MTA si nécessaire et instancie l'objet dedans.

Les Objets qui Supportent tous les Modèles d'Appartement

Même si un objet in-proc MTA est accédé par un client STA, que faire pour éviter le surcoût du marshalling ? Pour éviter cela, un composant in-proc peut déclarer son support pour tous les modèles de threading (ThreadingModel = Both). (Le terme Both est un ancien terme.) COM+ permet d'instancier ce type d'objet directement dans un STA, le NA ou le MTA, ce qui apporte un gain de performance par rapport aux composants MTA appelés par un STA. Comme les objets MTA, un objet qui supporte tous les types de modèle d'appartement doit posséder son propre mécanisme de synchronisation car il peut être accéder par plusieurs threads client en même temps.

En retour du privilège d'être instancié directement dans n'importe quel appartement, un composant in-proc supportant tous les modèles de threading ne doit pas faire d'appel direct en retour au client sur n'importe quel thread. En fait, il ne peut appeler le client en retour que sur le thread qui a reçu le pointeur d'interface. Donc, un objet qui supporte les trois modèles de threading déclare: "Vous pouvez m'appeler depuis n'importe quel thread, mais je vous rappellerais seulement sur le thread qui a reçu le pointeur d'interface."

Bien qu'une coclasse supporte les trois types d'appartement , à l'exécution chaque objet est instancié dans un STA, dans le NA ou dans le MTA—pas dans les trois. Si un objet marqué avec ThreadingModel = Both est instancié par un thread STA, il appartient à ce STA et tous les appels faits par les threads d'autres appartements doivent être marshalés, comme dans la Figure 4-7. Une autre option est d'instancier l'objet dans le MTA, permettant ainsi à tous les threads du MTA d'accéder directement à l'objet mais les accès depuis les STAs se font via le proxy. Finalement, si un objet marqué avec ThreadingModel = Both est instancié par un objet dans le NA, le nouvel objet est créé dans le NA.

Figure 4-7. Les appels a un composant in-process dans un appartement différent, même si le composant est enregistré avec ThreadingModel = Both, doivent être marshalés.

Les objets in-proc qui supportent les trois modèles de threading (ThreadingModel = Both) ne savent pas si ils seront créés dans un STA, le NT ou le MTA. Cependant, il serait intéressant de le savoir. Vous pouvez utiliser la fonction isMTA, décrite ci-dessous. La fonction isMTA retourne true si elle est appelée depuis un MTA, false si elle est appelée depuis un STA. Bien qu'une coclasse marquée avec ThreadingModel = Both peut être instanciée dans le NA, elle est toujours exécutée soit dans un STA ou MTA, suivant le modèle de threading du client.

bool isMTA()
{
    HRESULT hr = CoInitializeEx(NULL, COINIT_MULTITHREADED);
    if(hr == RPC_E_CHANGED_MODE)
        return false;
    else
        CoUninitialize();
    return true;
}

Le Free-Threaded Marshaler

Les composants in-proc déclarés avec ThreadingModel = Both sont conçu pour être instanciés dans n'importe quel type d'appartement; à l'exécution, ils sont toujours instanciés dans l'appartement du créateur. Cela garanti que le thread créateur peut appeler directement l'objet sans invoquer la paire proxy/stub. Cependant, les threads des autres appartements du processus utilisent le marshalling pour appeler l'objet. 

Bien que les objets in-proc déclarés avec ThreadingModel = Both sont conçus pour gérer les accès concurrents par différents clients, il semble que le client n'ai pas besoin de marshaller le pointeur d'interface de l'objet entre les appartements. Par exemple, si un objet qui supporte tous les modèles de threading est instancié par une thread MTA, l'objet est crée dans le MTA. Cela veut dire que tous les threads dans le MTA peuvent accéder à l'objet directement. Le client, cependant, doit suivre les règles COM+ et toujours marshaller les pointeurs d'interface entre les threads de différents appartements. Donc n'importe quel thread STA du processus a besoin d'un pointeur d'interface marshallé vers l'objet pour l'utiliser dans son appartement. Les threads STA accèdent ainsi à l'objet via un proxy bien que l'objet lui même est parfaitement capable d'accepter les appels direct d'un STA sans danger pour le thread STA. Dans cet exemple, le besoin pour les threads client de rester indépendant du modèle de threading d'un objet rentre en conflit avec le désire d'un développeur qui veut les meilleurs performances possibles.

Le free-threaded marshaler (FTM) est une optimisation technique conçu a cet égard. Le FTM permet à un objet in-proc de passer directement un pointeur d'interface dans n'importe quel appartement client. Quand un objet utilise le FTM, tous les threads client, STA ou MTA, appelle l'objet directement sans avoir à utiliser un proxy. La Figure 4-8 montre un thread STA (STA1) qui instancie un objet in-proc qui supporte tous les modèles de threading. STA1 appelle CoMarshalInterThreadInterfaceInStream pour marshaller le pointeur d'interface à un autre thread STA (STA2). STA2 appelle alors la fonction CoGetInterfaceAndReleaseStream pour obtenir le pointeur d'interface relatif pour utiliser l'objet. 

Normalement, STA2 a un pointeur vers un proxy qui marshale les appels au thread de STA1 avant l'appel de l'objet. Cependant, si l'objet en question utilise le FTM, STA2 reçoit un pointeur direct vers l'objet via la fonction CoGetInterfaceAndReleaseStream. Les autres threads du MTA ont aussi un accès direct à l'objet. Dans cet exemple, le choix de faire instancier l'objet par STA1 est arbitraire. Un thread appartenant à un appartement du processus pourrait instancier l'objet; le résultat serait identique: tous les appels in-proc sont directs quel que soit le type d'appartement du thread appelant. La seul différence est l'appartement dans lequel l'objet est créé. 

Figure 4-8. Un objet qui utilise le FTM permet aux threads de différents appartements du même processus d'accéder directement à l'objet.

Notez que cette situation montrée en Figure 4-8 est fondamentalement illégale: STA1, qui ne peut avoir un seul thread exécutant ses objets, a maintenant plusieurs threads de STA2 et de MTA qui accèdent directement à l'objet. Le FTM permet d'enfreindre les règles de threading COM+. Pour utiliser correctement le FTM,  vous devez bien comprendre les modèles de threading COM+ et les diverses situations dans lesquelles vos objets peuvent être appelés. Il est facile de provoquer le crash d'un processus en utilisant le FTM sans prendre de précaution.

Comment le FTM Fonctionne

Le FTM fonctionne en fournissant une implémentation de l'interface IMarshal qu'un objet expose via l'agrégation, comme dans la Figure 4-9. Quand un thread client marshale un pointeur d'interface en utilisant CoMarshalInterThreadInterfaceInStream, le système interroge l'objet pour l'interface IMarshal. Si l'objet agrège le FTM, l'implémentation FTM de l'interface IMarshal est retournée. Cette implémentation vérifie en premier lieu le type de marshalling.

Figure 4-9. L'objet InsideCOM qui agrège le FTM.

Si le marshaling prend place entre les appartements ou contextes d'un même processus (défini par les flags de contexte de marshalling MSHCTX_INPROC ou MSHCTX_ CROSSCTX, passés à la fonction CoMarshalInterface), le FTM copie simplement le pointeur d'interface vers l'objet dans le stream de marshaling. Quand un thread client différent d'un autre appartement unmarshalle le pointeur d'interface en appelant la fonction CoGetInterfaceAndReleaseStream, au lieu de recevoir un pointeur vers une interface proxy, le FTM récupère simplement le pointeur d'interface sur l'objet réel qui a été placé dans le stream. Le second thread client peut alors faire des appels directs à l'objet indépendamment du fait que l'objet s'exécute dans un appartement différent.

Si le contexte de marshaling n'est pas MSHCTX_INPROC ou MSHCTX_ CROSSCTX, cela indique que le marshalling se fait entre processus ou entre machines, donc le FTM délègue le travail de marshalling au marshaler standard obtenu en appelant CoGetStandardMarshal. Le marshaler standard charge les interfaces de proxy et stub appropriées et retourne un pointeur sur l'objet proxy au thread client. Le pseudo-code de la partie centrale du FTM est montrée ci-dessous:

HRESULT CFreeThreadedMarshaler::MarshalInterface(
    IStream* pStream, REFIID riid, void* pv, 
    DWORD dwDestContext, void* pvDestContext, DWORD dwFlags)
{
    // If cross-apartment or cross-context marshaling,...
    if(dwDestContext == MSHCTX_INPROC || 
        dwDestContext == MSHCTX_CROSSCTX)
        // simply store the pointer directly in the stream.
        return pStream->Write(this, sizeof(this), 0);

    // Otherwise, delegate the work to the standard marshaler.
    IMarshal* pMarshal = 0;
    CoGetStandardMarshal(riid, pv, dwDestContext, pvDestContext, 
        dwFlags, &pMarshal);
    HRESULT hr = pMarshal->MarshalInterface(pStream, riid, pv, 
        dwDestContext, pvDestContext, dwFlags);
    pMarshal->Release();
    return hr;
}

Agrégation du FTM

Le FTM est un objet implémenté par COM+; vous l'utilisez en appelant la fonction CoCreateFreeThreadedMarshaler. Le premier paramètre de la fonction CoCreateFreeThreadedMarshaler prend un pointeur sur le pointeur d'interface IUnknown de contrôle de l'objet, et le second paramètre retourne un pointeur sur l'implémentation de IUnknown du FTM. Le FTM est généralement agrégé dans le constructeur de l'objet:

CInsideCOM::CInsideCOM() : m_cRef(1)
{
    InterlockedIncrement(&g_cComponents);
    CoCreateFreeThreadedMarshaler(this, &m_pFTM);
}

Le pointeur sur le FTM retourné par la fonction CoCreateFreeThreadedMarshaler est stocké dans m_pFTM. Ce pointeur est requis dans l'implémentation IUnknown::QueryInterface de l'objet, qui délègue toutes les requêtes pour l'interface IMarshal vers les FTM. COM+ appele automatiquement QueryInterface pour obtenir l'interface IMarshal quand un pointeur d'interface sur cet objet est marshallé ou unmarshallé—par exemple, quand le client appele CoMarshalInterThreadInterfaceInStream ou CoGetInterfaceAndReleaseStream. La plupart des objets retournent E_NOINTERFACE, et COM+ fournit son marshaler standard. Cependant, dans notre cas, nous interceptons les requêtes pour l'interface IMarshal et les transférons au FTM. Le FTM retourne alors un pointeur sur son implémentation de IMarshal. Voici l'implémentation de IUnknown::QueryInterface de l'objet:

HRESULT CInsideCOM::QueryInterface(REFIID riid, void** ppv)
{
    if(riid == IID_IUnknown)
        *ppv = (IUnknown*)this;
    else if(riid == IID_ISum)
        *ppv = (ISum*)this;
    else if(riid == IID_IMarshal)
        return m_pFTM->QueryInterface(riid, ppv);
    else 
    {
        *ppv = NULL;
        return E_NOINTERFACE;
    }
    AddRef();
    return S_OK;
}

Avant la destruction de l'objet, le FTM doit être libéré. Cet appel est réalisé dans le destructeur de l'objet:

CInsideCOM::~CInsideCOM()
{
    InterlockedDecrement(&g_cComponents);
    m_pFTM->Release();
}

Problèmes avec FTM

Comme le FTM permet aux threads de différents appartements d'un processus de partager directement des pointeurs d'interface plutôt que d'utiliser des pointeurs sur des proxies—un violation volontaire des règles de threading COM+—cependant, un objet qui utilise le FTM doit prendre certaines précautions pour éviter le crash de l'application:

Imaginez un objet in-proc (OBJ1) qui supporte tous les modèles de threading (ThreadingModel = Both) et qui utilise le FTM, contient un pointeur sur un autre objet (OBJ2). C'est une source de problème possible car les objets utilisant le FTM ne peuvent pas maintenir des ressources indépendant du type d'appartement comme des références d'objet. Si un thread client d'un STA (STA1) marshale un pointeur d'interface sur OBJ1 vers un autre appartement (STA2), le FTM le considère comme un pointeur d'interface direct reçu par STA2.

Voici le problème: si STA2 appele une méthode de OBJ1 et que cette méthode appelle un méthode de OBJ2, cela casse les règles de threading pour OBJ2.  A moins que OBJ2 utilise le FTM, il ne peut pas être appelé directement depuis STA2 sans avoir été marshallé pour y être utilisé dans cet appartement. Cela provoque un erreur d'exécution ou, si OBJ2 est un proxy dans STA1 vers un objet out-proc, une erreur RPC_E_ WRONG_THREAD. La Figure 4-10 montre que les threads de STA1 et STA2 peuvent accéder à OBJ1 directement, mais que seul le thread de STA1 peut accéder à OBJ2.

Figure 4-10. Les règles de threading COM+ sont violées si STA2 appele OBJ1 et que OBJ1 appelle OBJ2 directement.

Le problème présenté dans la Figure 4-10 est typique, mais n'est pas facile à résoudre. Une solution possible consiste à appeler CoMarshalInterThreadInterfaceInStream après avoir instancié OBJ2. Cet appel retourne un stream avec une représentation relative du pointeur d'interface OBJ2 qui peut être unmarshallé en utilisant la fonction CoGetInterfaceAndReleaseStream pour obtenir un pointeur de fonction valide dans STA2. Le problème avec cette approche est que CoGetInterfaceAndReleaseStream unmarshalle le pointeur d'interface et libère le stream, ce qui veut dire que le pointeur d'interface ne peut être unmarshallé à partir du stream qu'une seule fois ! Tous les essais futurs pour unmarshaller le pointeur d'interface depuis le stream échoueront car il a déjà été libéré.

La Table d'Interface Globale (Global Interface Table)

La table d'interface globale (GIT) contient une table de pointeurs d'interface pour tous les processus. Les pointeurs d'interface peut être vérifiés dans la GIT, là ou ils sont disponibles pour chaque appartement du processus. Quand un thread demande un pointeur d'interface à la GIT, le pointeur d'interface retourné a la garantie d'être utilisable pour ce thread. Si nécessaire, la GIT invoque automatiquement le marshalling inter-appartement. Comme la GIT permet à un pointeur d'interface d'être unmarshallé autant de fois que c'est demandé, elle résout les problèmes qui peuvent survenir dans les objets utilisant le FTM.

La GIT est accessible via l'interface IGlobalInterfaceTable, décrite en IDL ci-dessous:

interface IGlobalInterfaceTable : IUnknown
{
    // Voluntarily check an interface pointer into the GIT.
    HRESULT RegisterInterfaceInGlobal
    (
        [in]  IUnknown* pUnk,
        [in]  REFIID    riid,
        [out] DWORD*    pdwCookie
    );

    // Remove an interface pointer from the GIT.
    HRESULT RevokeInterfaceFromGlobal
    (
        [in] DWORD      dwCookie
    );

    // Unmarshal an interface pointer from the GIT to the
    // caller's apartment.
    HRESULT GetInterfaceFromGlobal
    (
        [in]  DWORD                dwCookie,
        [in]  REFIID               riid,
        [out, iid_is(riid)] void** ppv
    );
};

La GIT fournit une implémentation adéquate de IGlobalInterfaceTable, donc vous n'avez pas besoin d'implémenter vous même cette interface. Pour instancier la GIT, vous appelez CoCreateInstance avec la paramètre CLSID positionné à CLSID_StdGlobalInterfaceTable, comme dans le code ci-dessous. Seulement une instance de la GIT existe par processus, donc les appels multiples à cette fonction retourne la même instance.

IGlobalInterfaceTable* m_pGIT;
CoCreateInstance(CLSID_StdGlobalInterfaceTable, NULL, 
    CLSCTX_INPROC_SERVER, IID_IGlobalInterfaceTable, 
    (void**)&m_pGIT);

Vous vérifiez les pointeurs d'interface dans la GIT en appelant la méthode IGlobalInterfaceTable::RegisterInterfaceInGlobal. Cette méthode stocke la référence neutre de l'objet dans la GIT et retourne un cookie que n'importe quel appartement peut utiliser pour obtenir un pointeur d'interface:

DWORD m_cookie;
m_pGIT->RegisterInterfaceInGlobal(pMyInterface, 
    IID_IMyInterface, &m_cookie);

Vous appelez la méthode IGlobalInterfaceTable::GetInterfaceFromGlobal dans le thread d'un autre appartement pour obtenir un pointeur d'interface relatif d'après un cookie retourné par RegisterInterfaceInGlobal; voir Figure 4-11. Le pointeur d'interface devra être libéré plus tard, mais n'importe quel appartement dans le processus qui veut obtenir un pointeur d'interface peut réutiliser ce cookie. La GIT va bien au delà de la limitation des fonctions CoMarshalInterThreadInterfaceInStream et CoGetInterfaceAndReleaseStream, qui ne permet le unmarshalling qu'une seule fois.

// Possibly called by a client thread in another apartment
m_pGIT->GetInterfaceFromGlobal(m_cookie, IID_IMyInterface, 
    (void**)&pMyInterface);

// Use pMyInterface here.

pMyInterface->Release();

Figure 4-11. Utiliser la GIT permet à un objet qui agrège le FTM de maintenir des références sur tous les types d'objets.

Le problème avec la GIT, est que chaque méthode invoquée par le client doit appeler IGlobalInterfaceTable::GetInterfaceFromGlobal pour obtenir le pointeur d'interface et IUnknown::Release pour le libérer avant de rendre la main au client, car l'objet n'a pas le droit de conserver des ressources relatives sur des appels de méthodes en ne sachant pas quel thread l'appele. Le unmarshaling de pointeurs d'interface depuis la GIT peut influer sur les performances

Avant de quitter, vous devez supprimer le pointeur d'interface de la GIT en appelant la méthode IGlobalInterfaceTable::RevokeInterfaceFromGlobal, comme dans le code ci-dessous. Il n'y a aucune restriction sur quel thread du processus peut appeler cette méthode. N'oubliez pas de libérer l'objet GIT après avoir fini de l'utiliser.

m_pGIT->RevokeInterfaceFromGlobal(m_cookie);

// Now release the GIT.
m_pGIT->Release();

Notez que l'application doit coordonner les accès à la GIT pour éviter qu'un thread du processus appel RevokeInterfaceFromGlobal pendant qu'un autre appele GetInterfaceFromGlobal pour le même cookie; la GIT ne fournit pas ce type de synchronisation.

En plus de résoudre les problèmes associés aux objets qui utilisent le FTM, la GIT offre un moyen élégant aux applications client pour marshaller les pointeurs d'interface entre les threads de différents appartements. La GIT remplace les appels aux fonction CoMarshalInterThreadInterfaceInStream et CoGetInterfaceAndReleaseStream dans les composants qui n'utilisent pas le FTM.

Les Appartements Neutre

Concevoir des coclasses marquées avec ThreadingModel = Both, qui utilisent le FTM et la GIT est assez compliqué. Il existe une autre alternative. Le modèle NA a été conçu pour permettre aux développeurs de créer des objets qui peuvent être appelés depuis n'importe quel thread de n'importe quel appartement dans un processus. Au lieu de casser les règles COM+, le modèle NA offre un moyen légal pour avoir les mêmes apports que les objets marqués avec ThreadingModel = Both, qui utilisent le FTM et la GIT. Le modèle NA est adapté aux composants COM+ non visuel.

Le modèle STA n'a qu'un seul thread, le modèle MTA peut en avoir plusieurs tandis que le modèle NA n'en a aucun. A la place, Les threads STA (synchronisés avec des queues de messages) et les threads MTA (non synchronisés) font toujours des appels direct aux objets qui tournent dans le NA, comme dans la Figure 412. Donc, les objets dans le NA sont toujours exécutés dans le thread de l'appelant. Bien que le NA ne fournit aucune synchronisation, quand il est appelé depuis un thread STA, la synchronisation est fourni via la boucle de message du modèle STA; quand il est appelé par un thread MTA alors aucune synchronisation n'est fourni. Comme le MTA, seul un NA existe existe dans un processus. Tous les objets marqués avec ThreadingModel = Neutral qui sont instanciés dans un processus partagent le NA.

Figure 4-12. Les threads de n'importe quel type d'appartement peuvent appeler les objets du NA sans switch de thread.

La fonction CoInitializeEx est utilisée pour lier les threads à un modèle de threading particulier, soit STA ou MTA; elle ne peut pas être utilisée pour créer le NA. Le seul moyen d'instancier un objet dans le NA est d'appeler CoGetClassObject ou CoCreateInstance(Ex) pour une coclasse marquée dans la base de registres avec ThreadingModel = Neutral. Comme avec les objets du MTA, les objets du NA n'ont pas d'affinité avec les threads car différents threads peuvent exécuter leur code. Cependant, les objets du NA sont relatif; ils peuvent contenir des ressources relatives comme des références à des objets. Ils différent des objets qui utilisent le FTM et la GIT pour maintenir des références à des objets; les objets du NA n'ont pas besoin d'utiliser le  FTM ou la GIT.

Les objets conçus pour le NA (ThreadingModel = Neutral) ont les mêmes règles d'implémentation que les objets marqué avec ThreadingModel = Both. Ils doivent gérer les accès concurrents aux données partagées par plusieurs threads. Ils ne peuvent utiliser le TLS ou tout autre affinité sur les threads.

Les Contextes

Les contextes sont utilisés dans COM+ pour conserver des informations à l'exécution (comme les objets qui requièrent une transaction) associées aux composants configurés qui utilisent les services de composants COM+. Les composants non configurés s'exécutent dans le contexte par défaut de chaque appartement à moins qu'ils soient créées par un composant configuré, et alors ils s'exécutent dans le contexte de l'appelant. Si vous considérez les contextes, la différence entre les objets marqués avec ThreadingModel = Both qui utilisent le FTM et la GIT et les objets du NA (ThreadingModel = Neutral) devient claire. Les objets qui utilisent le FTM peuvent être appelés directement depuis n'importe quel contexte de n'importe quel appartement de n'importe quel thread du processus. Les objets du NA peuvent être appelés directement (sans switch de thread) depuis n'importe quel thread de n'importe quel appartement dans le processus, mais ils sont sujets aux règles COM+ sur les appartements et les contextes.

Comparaison des Modèles d'Appartement

Bien qu'une variété de situations complexes puissent survenir entre les clients et les composants in-proc de différents modèles de threading, COM+ gère toutes ses situation. Seule une pénalité au niveau des performances existe si les modèles de threading des deux parties sont différents. Imaginez le cas ou un thread MTA accède à un objet STA. Dans ce cas, COM+ introduit une synchronisation pour protéger l'objet. Pour éviter cela, le client peut créer un STA à partir duquel il appele l'objet. Quand un client STA appele un objet STA, il n'y a pas de surcoût.

Comment déterminer les modèles de threading supportés par un composant? Pour les composants in-proc, vous devez examiner l'entrée ThreadingModel de l'objet dans la registry. Pour les composants out-proc, cette informations n'est pas disponible. C'est généralement moins important pour les clients et les composants out-proc d'utiliser le même modèle de threading car COM+ doit charger la paire proxy/stub pour le marshalling inter-processus dans tous les cas. La table ci-dessous montre les différents de threading qui peuvent être supportés par les clients et les composants in-proc et décrit comment COM+ gère chaque situation; la première colonne sur la gauche donne le type d'appartement du créateur; la première ligne donne la valeur ThreadingModel de la coclasse instanciée.

Non Spécifié Apartment Free Both Neutral
STA Principal Créé dans le STA principal. Accès direct. Créé dans le STA principal. Accès direct. Créé dans le MTA. Le MTA est créé si nécessaire. Accès proxy. Créé dans le STA principal. Accès direct. Créé dans le NA. Accès proxy léger—pas de thread switch.
STA Créé dans le STA principal. Accès proxy. Créé dans le STA de l'appelant. Accès direct. Créé dans le MTA. Le MTA est créé par le système si nécessaire. Accès proxy. Créé dans le STA due l'appelant. Accès direct. Créé dans le NA. Accès proxy léger—pas de thread switch.
MTA Créé dans le STA principal. Le STA principal est créé par le système si nécessaire. Accès proxy. Créé dans un host STA. Accès proxy. Créé dans le MTA. Accès direct. Créé dans le MTA. Accès direct. Créé dans le NA. Accès proxy léger—pas de thread switch.
NA (sur un thread STA) Créé dans le STA principal. Accès proxy. Créé dans le STA de l'appelant. Accès proxy léger—pas de switch. Créé dans le MTA. Le MTA est créé par le système si nécessaire. Accès proxy. Créé dans le NA. Accès direct. Créé dans le NA. Accès direct.
NA (sur un thread MTA) Créé dans le STA principal. Accès proxy. Créé dans un host STA. Accès proxy. Créé dans le MTA. Accès proxy léger—pas de thread switch. Créé dans le NA. Accès direct. Créé dans le NA. Accès direct.

Ecrire des Composants Thread-Safe

Après la théorie COM+ sur les modèles d'appartement, passons à la pratique. Cette section présente les techniques de coding à mettre en oeuvre pour réaliser des composants in-proc thread-safe.

Les objets in-proc qui supportent le modèle STA (ThreadingModel = Apartment) sont accédés par le même thread client qui a créé l'objet. Cependant, les objets STA peuvent être créer dans plusieurs STAs du processus client, tandis que les anciens objets sont toujours créés dans le STA principal. Comme plusieurs threads peuvent accéder à différents objets du composant en même temps, un composant STA doit coder les points d'entrées DllGetClassObject et DllCanUnloadNow en permettant les accès concurrents par plusieurs clients STAs.

Rendre DllGetClassObject et DllCanUnloadNow Thread-Safe

Imaginez deux STAs différents d'un processus client créés des instances de la même classe en même temps. Les deux threads accèdent à la fonction DllGetClassObject. Heureusement, les implémentations de DllGetClassObject, sont en général thread-safe car une nouvelle fabrique de classe est instanciée à chaque appel et aucune données globale ou statique n'est accédée. 

HRESULT __stdcall DllGetClassObject(REFCLSID clsid, 
    REFIID riid, void** ppv)
{
    if(clsid != CLSID_InsideCOM)
        return CLASS_E_CLASSNOTAVAILABLE;

    CFactory* pFactory = new CFactory;
     if(pFactory == NULL)
        return E_OUTOFMEMORY;

    HRESULT hr = pFactory->QueryInterface(riid, ppv);
    pFactory->Release();
    return hr;
}

DllCanUnloadNow doit suivre les mêmes règles. Bien que les composants in-proc ne gèrent pas leur propre cycle de vie, la fonction DllCanUnloadNow permet a un client de déterminer si la DLL peut être libérée. Si DllCanUnloadNow retourne S_OK, la DLL peut être libérée; si elle retourne S_FALSE, la DLL ne peut pas être libérée à ce point. La fonction DllCanUnloadNow est appelée par la fonction CoFreeUnusedLibraries, qui est appelée par les clients dans leur spare time. Voici une implémentation de DllCanUnloadNow:

HRESULT __stdcall DllCanUnloadNow()
{
    if(g_cServerLocks == 0 && g_cComponents == 0)
        return S_OK;
    else
        return S_FALSE;
}

Maintenant le client peut appeler la méthode IUnknown::Release, décrémentant le compteur d'usage à 0 et détruisant ainsi l'objet:

ULONG CInsideCOM::Release()
{
    if(--m_cRef != 0)
        return m_cRef;
    delete this;
    return 0;
}

Le destructeur décrémente la variable globale g_cComponents. Si le dernier objet est détruit, g_cComponents est décrémentée à 0. L'objet doit sortir du constructeur avant l'appel à DllCanUnloadNow. Si un autre thread dans le processus client appele CoFreeUnusedLibraries, DllCanUnloadNow retourne S_OK, laissant COM+ croire qu'il peut libérer la DLL. Cependant, libérer la DLL pendant l'exécution du destructeur provoque une erreur.

CInsideCOM::~CInsideCOM()
{
    g_cComponents--;
    // Pray nobody calls CoFreeUnusedLibraries now...
    // Some other cleanup code here...
}

Pour éviter ce genre de problème, Microsoft a modifié la fonction CoFreeUnusedLibraries. Au lieu de libérer immédiatement la DLL lorsque DllCanUnloadNow retourne S_OK, CoFreeUnusedLibraries attend 10 minutes. Si CoFreeUnusedLibraries est appelé après et DllCanUnloadNow retourne encore S_OK, alors et seulement la DLL est libérée. Cette approche fonctionne correctement; en effet, connaissez vous un constructeur qui s'exécute pendant plus de 10 minutes?