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 -Konstrukten "Modulstatus" beschrieben. Ein Verständnis der Modulstatusimplementierung ist für die Verwendung der freigegebenen MFC-DLLs von einer DLL (oder OLE-In-Process-Server) wichtig.

Bevor Sie diese Notiz lesen, lesen Sie "Verwalten der Statusdaten von MFC-Modulen" in der Erstellung neuer Dokumente, Windows und Ansichten. Dieser Artikel enthält wichtige Nutzungsinformationen und Übersichtsinformationen zu diesem Thema.

Übersicht

Es gibt drei Arten von MFC-Statusinformationen: Modulstatus, Prozessstatus und Threadstatus. Manchmal können diese Zustandstypen kombiniert werden. Beispielsweise sind die Handle-Karten von MFC sowohl lokal als auch thread lokal. Dies ermöglicht zwei verschiedene Module, unterschiedliche Karten in jedem ihrer Threads zu haben.

Der Prozessstatus und der Threadstatus sind ähnlich. Diese Datenelemente sind Dinge, die traditionell globale Variablen waren, aber für einen bestimmten Prozess oder Thread für die ordnungsgemäße Unterstützung von Win32s oder für die richtige Multithreading-Unterstützung spezifisch sein müssen. Welche Kategorie ein bestimmtes Datenelement passt, hängt von diesem Element und seinen gewünschten Semantik hinsichtlich Prozess- und Threadgrenzen ab.

Der Modulzustand ist eindeutig, dass er entweder einen wirklich globalen Zustand oder Zustand enthält, der lokal oder thread lokal verarbeitet wird. Darüber hinaus kann es schnell umgestellt werden.

Modulstatuswechsel

Jeder Thread enthält einen Zeiger auf den Status "current" oder "active" (nicht überraschend, der Zeiger ist Teil des lokalen Threadzustands von MFC). Dieser Zeiger wird geändert, wenn der Thread der Ausführung eine Modulgrenze übergibt, z. B. eine Anwendung, die in ein OLE-Steuerelement oder eine OLE-Steuerelement aufgerufen wird, die wieder in eine Anwendung aufgerufen wird.

Der aktuelle Modulstatus wird durch Aufrufen AfxSetModuleStategewechselt. Für die meisten Teile werden Sie nie direkt mit der API umgehen. MFC wird sie in vielen Fällen für Sie aufrufen (bei WinMain, OLE-Einstiegspunkten, AfxWndProcusw.). Dies erfolgt in jeder Komponente, die Sie schreiben, indem Sie eine statische Verknüpfung in einem speziellen Element erstellen, und ein spezieller WndProcWinMain (oder DllMain) der weiß, welcher Modulzustand aktuell sein sollte. 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 Modulzustand festlegen möchten und dann nicht zurückgesetzt werden. Die meisten Zeit, die Sie als aktuelles Modul "pushen" möchten, und dann, nachdem Sie fertig sind, "pop" den ursprünglichen Kontext zurück. 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 OLE-Automatisierung und OLE COM-Einstiegspunkte verwendet wird. Wie alle anderen Einstiegspunkte, die dem System verfügbar gemacht werden, müssen diese Einstiegspunkte den richtigen Modulzustand festlegen. Wie weiß ein bestimmtes CCmdTarget Wissen, was der "richtige" Modulzustand sein sollte Die Antwort ist, dass es sich daran erinnert, was der Status des "aktuellen" Moduls ist, wenn es erstellt wird, sodass er den aktuellen Modulzustand auf diesen "gespeicherten" Wert festlegen kann, wenn er später aufgerufen wird. Dadurch ist der Modulzustand, dem ein bestimmtes CCmdTarget Objekt zugeordnet ist, der Modulzustand, der beim Erstellen des Objekts aktuell war. Nehmen Sie ein einfaches Beispiel zum Laden eines INPROC-Servers, zum Erstellen eines Objekts und zum Aufrufen seiner Methoden.

  1. Die DLL wird mithilfe von OLE LoadLibrarygeladen.

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

  3. Der Konstruktor für die Klassenfabrik, die unserem Objekt zugeordnet ist, wird aufgerufen. COleObjectFactory es wird von CCmdTarget und als Ergebnis abgeleitet, es erinnert sich daran, in welchem Modulzustand es instanziiert wurde. Dies ist wichtig – wenn die Klassenfabrik aufgefordert wird, Objekte zu erstellen, weiß es jetzt, welchen Modulstatus sie aktuell machen soll.

  4. DllGetClassObject wird aufgerufen, um die Klassenfabrik abzurufen. MFC sucht die Klassenfabrikliste, die diesem Modul zugeordnet ist, und gibt ihn zurück.

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

  6. Wenn das Objekt erstellt wird, ist es auch ein CCmdTarget abgeleitetes und auf dieselbe Weise COleObjectFactory daran erinnert, welche Modulstatus aktiv war, also dieses neue Objekt. Jetzt weiß das Objekt, in welchen Modulzustand es wechselt, wann immer er aufgerufen wird.

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

Wie Sie sehen können, wird der Modulzustand von Objekt zu Objekt verteilt, da sie erstellt werden. Es ist wichtig, dass der Modulzustand entsprechend festgelegt wird. Wenn sie nicht festgelegt ist, kann Ihr DLL- oder COM-Objekt schlecht mit einer MFC-Anwendung interagieren, die sie aufruft, oder möglicherweise nicht ihre eigenen Ressourcen finden oder möglicherweise auf andere miserable Weise fehlschlagen.

Beachten Sie, dass bestimmte Arten von DLLs, insbesondere "MFC-Erweiterung"-DLLs, nicht den Modulstatus in ihrer RawDllMain (tatsächlich, sie haben normalerweise keine RawDllMain). Dies liegt daran, dass sie sich "wie wenn" verhalten sollen, die sie tatsächlich in der Anwendung vorhanden waren, die sie verwendet. Sie sind sehr viel 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 aufruften Anwendung nicht ändern; die Anwendung, die sie aufruft, ist möglicherweise nicht einmal eine MFC-Anwendung und es gibt möglicherweise keinen Zustand, der geändert werden soll. Dies ist der Grund, warum die Modulzustandswechsel erfunden wurde.

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

AFX_MANAGE_STATE(AfxGetStaticModuleState())

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

Probleme mit Ressourcen in DLLs treten auf, wenn das AFX_MODULE_STATE Makro nicht verwendet wird. Standardmäßig verwendet MFC den Ressourcenhandpunkt der Hauptanwendung, um die Ressourcenvorlage zu laden. Diese Vorlage wird tatsächlich in der DLL gespeichert. Die Ursache besteht darin, dass die Modulstatusinformationen von MFC nicht vom AFX_MODULE_STATE Makro gewechselt wurden. Der Ressourcenhandpunkt wird vom Modulstatus von MFC wiederhergestellt. Durch das Wechseln des Modulzustands wird der falsche Ressourcenhandpunkt verwendet.

AFX_MODULE_STATE muss nicht in jede Funktion in der DLL platziert werden. So kann z. B InitInstance . der MFC-Code in der Anwendung ohne AFX_MODULE_STATE aufgerufen werden, da MFC den Modulzustand InitInstance automatisch verschiebt und dann nach InitInstance rückgaben zurückschaltet. Das gleiche gilt für alle Nachrichtenzuordnungshandler. Reguläre MFC-DLLs verfügen tatsächlich über eine spezielle Masterfensterprozedur, die den Modulzustand automatisch wechselt, bevor eine Nachricht weitergeleitet wird.

Verarbeiten lokaler Daten

Die Verarbeitung lokaler Daten wäre nicht von großem Interesse, da es sich nicht um die Schwierigkeit des Win32s-DLL-Modells handelte. In Win32s teilen alle DLLs ihre globalen Daten, auch wenn sie von mehreren Anwendungen geladen werden. Dies unterscheidet sich sehr von dem "echten" Win32-DLL-Datenmodell, in dem jede DLL eine separate Kopie des Datenraums in jedem Prozess erhält, der an die DLL angefügt wird. Um der Komplexität hinzuzufügen, sind Daten, die auf dem Heap in einer Win32s-DLL zugewiesen wurden, tatsächlich prozessspezifisch (zumindest so weit wie der Besitz geht). Berücksichtigen Sie die folgenden Daten und 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);
}

Berücksichtigen Sie, was geschieht, wenn sich der obige Code in einer DLL befindet und die DLL von zwei Prozessen A und B geladen wird (es könnte tatsächlich zwei Instanzen derselben Anwendung sein). Ein Aufruf SetGlobalString("Hello from A"). Daher wird der Speicher für die CString Daten im Kontext des Prozesses A zugewiesen. Beachten Sie, dass das CString selbst global ist und sowohl für A als auch für B sichtbar ist. Jetzt ruft B-Anrufe GetGlobalString(sz, sizeof(sz))an. B kann die Daten sehen, die A festgelegt haben. 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, eine Anwendung auf globale Daten zu auswirken, die als Besitz einer anderen Anwendung angesehen werden.

Es gibt auch zusätzliche Probleme. Sagen wir, dass A jetzt beendet wird. Wenn A beendet wird, wird der von der Zeichenfolge 'strGlobal' verwendete Speicher für das System verfügbar gemacht – das heißt, alle von Prozess A zugewiesenen Speicher werden automatisch vom Betriebssystem freigestellt. Es ist nicht frei, weil der CString Destruktor aufgerufen wird; es wurde noch nicht aufgerufen. Es wird einfach freigelassen, weil die Anwendung, die sie zugewiesen hat, die Szene verlassen hat. Wenn B nun aufgerufen GetGlobalString(sz, sizeof(sz))wird, wird möglicherweise keine gültigen Daten abgerufen. Einige andere Anwendung hat möglicherweise diesen Speicher für etwas anderes verwendet.

Ein Problem ist eindeutig vorhanden. MFC 3.x verwendete eine Technik namens thread-local storage (TLS). MFC 3.x würde einen TLS-Index zuweisen, der unter Win32s wirklich 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 lokaler Thread-lokalen Daten in Win32 verwendet wurde (siehe unten, um weitere Informationen zu diesem Thema zu finden). Dies führte dazu, dass jede MFC-DLL mindestens zwei TLS-Indizes pro Prozess verwendet. Wenn Sie viele OLE Control DLLs (OCXs) laden, laufen Sie schnell aus TLS-Indizes (es gibt 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 für die Verwendung von TLS-Indizes.

MFC 4.x adressieren dies mit einer Reihe von Klassenvorlagen, die Sie um die Daten umbrechen können, die lokal verarbeitet werden sollen. Beispielsweise könnte das oben genannte Problem 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 oben in der 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 bereitgestellt, um auf diese Daten zuzugreifen. Es überschreibt den Operator -> was die intuitive Syntax ermöglicht, die Sie oben sehen. Alle Objekte, die durch CProcessLocal umgebrochen werden, müssen aus CNoTrackObjectabgeleitet werden. CNoTrackObjectstellt einen niedrigeren Allocator (LocalAllocLocalFree/) und einen virtuellen Destruktor bereit, damit MFC die lokalen Prozesse automatisch zerstören kann, wenn der Prozess beendet wird. Solche Objekte können einen benutzerdefinierten Destruktor haben, wenn zusätzliche Bereinigung erforderlich ist. Das obige Beispiel erfordert keines, 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 erstellt, wenn sie benötigt werden. CProcessLocal::operator-> instanziiert das zugeordnete Objekt zum ersten Mal, wenn es aufgerufen wird, und noch nicht früher. Im obigen Beispiel bedeutet das, dass die Zeichenfolge 'strGlobal' erst erstellt wird, wenn sie zum ersten SetGlobalString Mal aufgerufen wird oder GetGlobalString aufgerufen wird. In einigen Fällen kann dies dazu beitragen, die DLL-Startzeit zu verringern.

Thread-lokale Daten

Ähnlich wie bei der Verarbeitung lokaler Daten wird thread local data verwendet, wenn die Daten auf 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 freigegeben 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 ihn 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 von zwei verschiedenen Threads aufgerufen wurde, würde jede die Zeichenfolge auf unterschiedliche Weise zuweisen, ohne die andere zu beeinträchtigen. Dies liegt daran, dass anstelle einer globalen Instanz tatsächlich eine strThread Instanz pro Thread vorhanden ist.

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

Die CThreadLocal Klassenvorlage verwendet dieselben Mechanismen, CProcessLocal die die gleichen Implementierungstechniken ausführt und die gleichen Implementierungstechniken.

Siehe auch

Technische Notizen nach Zahl
Technische Notizen nach Kategorie