TN058: MFC-Modulzustandsimplementierung

Hinweis

Der folgende technische Hinweis wurde seit dem ersten Erscheinen in der Onlinedokumentation nicht aktualisiert. Daher können einige Verfahren und Themen veraltet oder falsch sein. Um aktuelle Informationen zu erhalten, wird empfohlen, das gewünschte Thema im Index der Onlinedokumentation zu suchen.

In diesem technischen Hinweis wird die Implementierung von MFC-"Modulstatus"-Konstrukten beschrieben. Ein Verständnis der Modulstatusimplementierung ist für die Verwendung der gemeinsam genutzten MFC-DLLs von einer DLL (oder OLE-In-Process-Server) wichtig.

Lesen Sie vor dem Lesen dieses Hinweises unter "Managing the State Data of MFC Modules" in Creating New Documents, Windows und Views. Dieser Artikel enthält wichtige Nutzungsinformationen und Übersichtsinformationen zu diesem Thema.

Überblick

Es gibt drei Arten von MFC-Statusinformationen: Modulstatus, Prozessstatus und Threadstatus. Manchmal können diese Zustandstypen kombiniert werden. Beispielsweise sind die Handlezuordnungen von MFC sowohl lokale Module als auch Thread lokal. Dadurch können zwei unterschiedliche Module in jedem ihrer Threads unterschiedliche Zuordnungen aufweisen.

Prozessstatus und Threadstatus sind ähnlich. Bei diesen Datenelementen handelt es sich um Elemente, die traditionell globale Variablen waren, aber für einen bestimmten Prozess oder Thread spezifisch sein müssen, um eine ordnungsgemäße Win32s-Unterstützung oder eine ordnungsgemäße Unterstützung für Multithreading zu erhalten. Welche Kategorie ein bestimmtes Datenelement passt, hängt von diesem Element und der gewünschten Semantik im Hinblick auf Prozess- und Threadgrenzen ab.

Der Modulstatus ist eindeutig, da er entweder einen wirklich globalen Zustand oder einen Zustand enthalten kann, der lokal oder Thread lokal verarbeitet wird. Darüber hinaus kann es schnell umgeschaltet werden.

Modulstatuswechsel

Jeder Thread enthält einen Zeiger auf den "aktuellen" oder "aktiven" Modulzustand (überraschenderweise ist der Zeiger Teil des lokalen MFC-Threadzustands). Dieser Zeiger wird geändert, wenn der Ausführungsthread eine Modulgrenze übergibt, z. B. eine Anwendung, die ein OLE-Steuerelement oder eine DLL aufruft, oder ein OLE-Steuerelement, das wieder in eine Anwendung aufruft.

Der aktuelle Modulstatus wird durch Aufrufen AfxSetModuleStategewechselt. In den meisten Fällen befassen Sie sich nie direkt mit der API. MFC wird es in vielen Fällen für Sie aufrufen (bei WinMain, OLE-Einstiegspunkten, AfxWndProcusw.). Dies erfolgt in jeder Komponente, die Sie schreiben, indem Sie statisch eine Verknüpfung in einem speziellen Element erstellen, und einem speziellen WndProcWinMain (oder DllMain), der weiß, welcher Modulstatus aktuell sein soll. Sie können diesen Code sehen, indem Sie sich DLLMODUL ansehen. CPP oder APPMODUL. CPP im MFC\SRC-Verzeichnis.

Es ist selten, dass Sie den Modulstatus festlegen und dann nicht wieder festlegen möchten. Meistens möchten Sie ihren eigenen Modulstatus als aktuellen "pushen" und dann nach Abschluss des Vorgangs den ursprünglichen Kontext wieder "auffüllen". Dies erfolgt durch das Makro AFX_MANAGE_STATE und die spezielle Klasse AFX_MAINTAIN_STATE.

CCmdTarget verfügt über spezielle Features zur Unterstützung des Modulzustandswechsels. Insbesondere ist eine CCmdTarget Stammklasse, die für die OLE-Automatisierung und OLE COM-Einstiegspunkte verwendet wird. Wie jeder andere Einstiegspunkt, der dem System verfügbar gemacht wird, müssen diese Einstiegspunkte den richtigen Modulzustand festlegen. Wie weiß ein bestimmtes CCmdTarget Wissen, was der "richtige" Modulstatus sein sollte Die Antwort ist, dass er sich "merkt", was der "aktuelle" Modulstatus ist, wenn es erstellt wird, sodass er den aktuellen Modulstatus auf diesen "gespeicherten" Wert festlegen kann, wenn er später aufgerufen wird. Daher ist der Modulstatus, dem ein bestimmtes CCmdTarget Objekt zugeordnet ist, der Modulstatus, der beim Erstellen des Objekts aktuell war. Nehmen Sie sich ein einfaches Beispiel für das Laden eines INPROC-Servers, das Erstellen eines Objekts und das Aufrufen der zugehörigen Methoden an.

  1. Die DLL wird mithilfe von OLE LoadLibrarygeladen.

  2. RawDllMain wird zuerst aufgerufen. Er legt den Modulstatus auf den bekannten statischen Modulzustand für die DLL fest. Aus diesem Grund RawDllMain ist statisch mit der DLL verknüpft.

  3. Der Konstruktor für die Klassenfactory, die unserem Objekt zugeordnet ist, wird aufgerufen. COleObjectFactory wird von CCmdTarget und als Ergebnis abgeleitet, wird daran erinnert, in welchem Modulzustand sie instanziiert wurde. Dies ist wichtig – wenn die Klassenfactory zum Erstellen von Objekten aufgefordert wird, weiß es jetzt, welcher Modulstatus aktuell ist.

  4. DllGetClassObject wird aufgerufen, um die Klassenfactory abzurufen. MFC durchsucht die diesem Modul zugeordnete Klassenfactoryliste und gibt sie zurück.

  5. COleObjectFactory::XClassFactory2::CreateInstance wird aufgerufen. Bevor Sie das Objekt erstellen und es zurückgeben, legt diese Funktion den Modulstatus auf den Modulstatus fest, der in Schritt 3 aktuell war (die, die beim COleObjectFactory Instanziieren des Objekts aktuell war). Dies geschieht innerhalb von METHOD_PROLOGUE.

  6. Wenn das Objekt erstellt wird, handelt es sich ebenfalls um ein CCmdTarget Abgeleitetes und auf die gleiche Weise COleObjectFactory daran erinnert, welcher Modulstatus aktiv war. Dies geschieht also mit diesem neuen Objekt. Jetzt weiß das Objekt, zu welchem Modulzustand gewechselt werden soll, wann immer es aufgerufen wird.

  7. Der Client ruft eine Funktion für das OLE COM-Objekt auf, das er von seinem CoCreateInstance Aufruf empfangen hat. Wenn das Objekt aufgerufen wird, wird er METHOD_PROLOGUE verwendet, um den Modulzustand wie COleObjectFactory folgt zu wechseln.

Wie Sie sehen können, wird der Modulstatus beim Erstellen vom Objekt in das Objekt weitergegeben. Es ist wichtig, dass der Modulstatus entsprechend festgelegt ist. Wenn sie nicht festgelegt ist, interagiert Ihr DLL- oder COM-Objekt möglicherweise schlecht mit einer MFC-Anwendung, die sie aufruft, oder kann ihre eigenen Ressourcen nicht finden oder auf andere schlechte Weise fehlschlagen.

Beachten Sie, dass bestimmte Arten von DLLs, insbesondere "MFC Extension"-DLLs, den Modulstatus nicht in ihren RawDllMain (tatsächlich, sie haben normalerweise nicht einmal ein RawDllMain). Dies liegt daran, dass sie sich so verhalten sollen, als wären sie tatsächlich in der Anwendung vorhanden, die sie verwendet. Sie sind sehr ein Teil der Anwendung, die ausgeführt wird, und es ist ihre Absicht, den globalen Zustand dieser Anwendung zu ändern.

OLE-Steuerelemente und andere DLLs unterscheiden sich sehr. Sie möchten den Zustand der aufrufenden Anwendung nicht ändern; die Anwendung, die sie aufruft, ist möglicherweise nicht einmal eine MFC-Anwendung, und daher kann kein Zustand geändert werden. Dies ist der Grund, warum modulzustandswechsel erfunden wurde.

Für exportierte Funktionen aus einer DLL, z. B. eines, das ein Dialogfeld in Ihrer DLL startet, müssen Sie den folgenden Code am Anfang der Funktion hinzufügen:

AFX_MANAGE_STATE(AfxGetStaticModuleState())

Dadurch wird der aktuelle Modulstatus durch den Zustand ausgetauscht, der von AfxGetStaticModuleState zurückgegeben wird, bis zum Ende des aktuellen Bereichs.

Probleme mit Ressourcen in DLLs treten auf, wenn das AFX_MODULE_STATE Makro nicht verwendet wird. Standardmäßig verwendet MFC das Ressourcenhandle der Standard Anwendung, um die Ressourcenvorlage zu laden. Diese Vorlage wird tatsächlich in der DLL gespeichert. Die Ursache ist, dass die Modulstatusinformationen von MFC nicht vom AFX_MODULE_STATE-Makro gewechselt wurden. Der Ressourcenhandle wird aus dem Modulstatus von MFC wiederhergestellt. Wenn Sie den Modulstatus nicht wechseln, wird das falsche Ressourcenhandle verwendet.

AFX_MODULE_STATE muss nicht in jede Funktion in der DLL platziert werden. Kann z. B. vom MFC-Code in der Anwendung ohne AFX_MODULE_STATE aufgerufen werden, InitInstance da MFC den Modulzustand InitInstance automatisch vor und dann wieder zurückschaltet InitInstance . Dasselbe gilt für alle Nachrichtenzuordnungshandler. Normale MFC-DLLs verfügen tatsächlich über eine spezielle Masterfensterprozedur, mit der der Modulstatus automatisch vor dem Weiterleiten einer Nachricht gewechselt wird.

Verarbeiten lokaler Daten

Die Verarbeitung lokaler Daten wäre nicht so beunruhigend, wenn es nicht um die Schwierigkeit des Win32s-DLL-Modells ging. In Win32s teilen alle DLLs ihre globalen Daten, auch wenn sie von mehreren Anwendungen geladen werden. Dies unterscheidet sich sehr vom "echten" Win32 DLL-Datenmodell, bei dem jede DLL eine separate Kopie des Datenbereichs in jedem Prozess erhält, der an die DLL angefügt wird. Um die Komplexität zu erhöhen, sind daten, die dem Heap in einer Win32s-DLL zugeordnet sind, tatsächlich prozessspezifisch (zumindest soweit der Besitz geht). Berücksichtigen Sie die folgenden Daten und den folgenden Code:

static CString strGlobal; // at file scope

__declspec(dllexport)
void SetGlobalString(LPCTSTR lpsz)
{
    strGlobal = lpsz;
}

__declspec(dllexport)
void GetGlobalString(LPCTSTR lpsz, size_t cb)
{
    StringCbCopy(lpsz, cb, strGlobal);
}

Überlegen Sie, was passiert, wenn sich der obige Code in einer DLL befindet und diese DLL von zwei Prozessen A und B geladen wird (es kann tatsächlich zwei Instanzen derselben Anwendung sein). Ein Aufruf SetGlobalString("Hello from A"). Daher wird speicher für die CString Daten im Kontext von Prozess A zugeordnet. Denken Sie daran, dass das CString selbst global ist und sowohl für A als auch für B sichtbar ist. Jetzt ruft B an GetGlobalString(sz, sizeof(sz)). B kann die Daten sehen, die von A festgelegt wurden. Dies liegt daran, dass Win32s keinen Schutz zwischen Prozessen wie Win32 bietet. Das ist das erste Problem; in vielen Fällen ist es nicht wünschenswert, dass eine Anwendung globale Daten beeinflusst, die als Eigentum einer anderen Anwendung angesehen werden.

Es gibt auch zusätzliche Probleme. Nehmen wir an, dass A jetzt beendet wird. Wenn A beendet wird, wird der von der Zeichenfolge "strGlobal" verwendete Speicher für das System verfügbar gemacht, d. h., der gesamte speicher, der von Prozess A zugewiesen wird, wird automatisch vom Betriebssystem freigegeben. Es wird nicht freigegeben, weil der CString Destruktor aufgerufen wird; er wurde noch nicht aufgerufen. Sie wird einfach freigegeben, weil die zugewiesene Anwendung die Szene verlassen hat. Wenn B aufgerufen wird GetGlobalString(sz, sizeof(sz)), werden möglicherweise keine gültigen Daten abgerufen. Einige andere Anwendung hat diesen Speicher möglicherweise für etwas anderes verwendet.

Es besteht eindeutig ein Problem. MFC 3.x verwendete eine Technik namens thread-local storage (TLS). MFC 3.x würde einen TLS-Index zuweisen, der unter Win32s tatsächlich als prozesslokaler Speicherindex fungiert, obwohl es nicht aufgerufen wird und dann auf alle Daten basierend auf diesem TLS-Index verweist. Dies ähnelt dem TLS-Index, der zum Speichern von threadlokalen Daten in Win32 verwendet wurde (weitere Informationen zu diesem Thema finden Sie unten). Dies führte dazu, dass jede MFC-DLL mindestens zwei TLS-Indizes pro Prozess verwendet. Wenn Sie das Laden vieler OLE Control DLLs (OCXs) berücksichtigen, sind tls-Indizes schnell nicht mehr verfügbar (es sind nur 64 verfügbar). Darüber hinaus musste MFC alle diese Daten an einem Ort in einer einzigen Struktur platzieren. Es war nicht sehr erweiterbar und war nicht ideal in Bezug auf die Verwendung von TLS-Indizes.

MFC 4.x behebt dies mit einer Reihe von Klassenvorlagen, die Sie um die Daten "umbrechen" können, die lokal verarbeitet werden sollen. Das oben Erwähnung problem könnte beispielsweise durch Schreiben behoben werden:

struct CMyGlobalData : public CNoTrackObject
{
    CString strGlobal;
};
CProcessLocal<CMyGlobalData> globalData;

__declspec(dllexport)
void SetGlobalString(LPCTSTR lpsz)
{
    globalData->strGlobal = lpsz;
}

__declspec(dllexport)
void GetGlobalString(LPCTSTR lpsz, size_t cb)
{
    StringCbCopy(lpsz, cb, globalData->strGlobal);
}

MFC implementiert dies in zwei Schritten. Zunächst gibt es eine Ebene über den Win32 Tls* -APIs (TlsAlloc, TlsSetValue, TlsGetValue usw.), die nur zwei TLS-Indizes pro Prozess verwenden, unabhängig davon, wie viele DLLs Sie haben. Zweitens wird die CProcessLocal Vorlage für den Zugriff auf diese Daten bereitgestellt. Es überschreibt den Operator–> was die intuitive Syntax ermöglicht, die Sie oben sehen. Alle Objekte, die umschlossen CProcessLocal werden, müssen von CNoTrackObject. CNoTrackObjectstellt einen Allocator auf niedrigerer Ebene (LocalAlloc/LocalFree) und einen virtuellen Destruktor bereit, sodass MFC die lokalen Prozessobjekte automatisch zerstören kann, wenn der Prozess beendet wird. Solche Objekte können einen benutzerdefinierten Destruktor aufweisen, wenn zusätzliche sauber up erforderlich ist. Im obigen Beispiel ist keines erforderlich, da der Compiler einen Standarddestruktor generiert, um das eingebettete CString Objekt zu zerstören.

Es gibt weitere interessante Vorteile für diesen Ansatz. Nicht nur alle CProcessLocal Objekte werden automatisch zerstört, sie werden erst konstruiert, wenn sie benötigt werden. CProcessLocal::operator-> instanziiert das zugeordnete Objekt, wenn es zum ersten Mal aufgerufen wird, und nicht früher. Im obigen Beispiel bedeutet dies, dass die Zeichenfolge "strGlobal" erst erstellt wird, wenn sie zum ersten Mal SetGlobalString aufgerufen oder GetGlobalString aufgerufen wird. In einigen Fällen kann dies dazu beitragen, die DLL-Startzeit zu verringern.

Lokale Threaddaten

Ähnlich wie beim Verarbeiten lokaler Daten werden threadlokale Daten verwendet, wenn die Daten in einem bestimmten Thread lokal sein müssen. Das heißt, Sie benötigen eine separate Instanz der Daten für jeden Thread, der auf diese Daten zugreift. Dies kann oft anstelle umfangreicher Synchronisierungsmechanismen verwendet werden. Wenn die Daten nicht von mehreren Threads gemeinsam genutzt werden müssen, können solche Mechanismen teuer und unnötig sein. Angenommen, wir hatten ein CString Objekt (ähnlich wie im obigen Beispiel). Wir können den Thread lokal gestalten, indem wir ihn mit einer CThreadLocal Vorlage umschließen:

struct CMyThreadData : public CNoTrackObject
{
    CString strThread;
};
CThreadLocal<CMyThreadData> threadData;

void MakeRandomString()
{
    // a kind of card shuffle (not a great one)
    CString& str = threadData->strThread;
    str.Empty();
    while (str.GetLength() != 52)
    {
        unsigned int randomNumber;
        errno_t randErr;
        randErr = rand_s(&randomNumber);

        if (randErr == 0)
        {
            TCHAR ch = randomNumber % 52 + 1;
            if (str.Find(ch) <0)
            str += ch; // not found, add it
        }
    }
}

Wenn MakeRandomString sie aus zwei verschiedenen Threads aufgerufen wurde, würde jede die Zeichenfolge auf unterschiedliche Weise "shuffle", ohne die andere zu beeinträchtigen. Dies liegt daran, dass es tatsächlich eine strThread Instanz pro Thread statt nur einer globalen Instanz gibt.

Beachten Sie, wie ein Verweis verwendet wird, um die CString Adresse einmal statt einmal pro Schleifeniteration zu erfassen. Der Schleifencode könnte mit threadData->strThread überallstr "" geschrieben worden sein, aber der Code wäre bei der Ausführung viel langsamer. Es empfiehlt sich, einen Verweis auf die Daten zwischenzuspeichern, wenn solche Verweise in Schleifen auftreten.

Die CThreadLocal Klassenvorlage verwendet dieselben Mechanismen wie CProcessLocal die gleichen Implementierungstechniken.

Siehe auch

Technische Hinweise – nach Nummern geordnet
Technische Hinweise – nach Kategorien geordnet