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 AfxSetModuleState
gewechselt. 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, AfxWndProc
usw.). Dies erfolgt in jeder Komponente, die Sie schreiben, indem Sie eine statische Verknüpfung in einem speziellen Element erstellen, und ein spezieller WndProc
WinMain
(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.
Die DLL wird mithilfe von OLE
LoadLibrary
geladen.RawDllMain
wird zuerst aufgerufen. Es legt den Modulstatus auf den bekannten statischen Modulstatus für die DLL fest. Aus diesem GrundRawDllMain
wird die DLL statisch verknüpft.Der Konstruktor für die Klassenfabrik, die unserem Objekt zugeordnet ist, wird aufgerufen.
COleObjectFactory
es wird vonCCmdTarget
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.DllGetClassObject
wird aufgerufen, um die Klassenfabrik abzurufen. MFC sucht die Klassenfabrikliste, die diesem Modul zugeordnet ist, und gibt ihn zurück.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 aktuellCOleObjectFactory
war). Dies erfolgt innerhalb von METHOD_PROLOGUE.Wenn das Objekt erstellt wird, ist es auch ein
CCmdTarget
abgeleitetes und auf dieselbe WeiseCOleObjectFactory
daran erinnert, welche Modulstatus aktiv war, also dieses neue Objekt. Jetzt weiß das Objekt, in welchen Modulzustand es wechselt, wann immer er aufgerufen wird.Der Client ruft eine Funktion auf dem OLE COM-Objekt auf, das er von seinem
CoCreateInstance
Aufruf empfangen hat. Wenn das Objekt aufgerufen wird, wirdMETHOD_PROLOGUE
er verwendet, um den Modulzustand genau wie folgtCOleObjectFactory
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 CNoTrackObject
abgeleitet werden. CNoTrackObject
stellt 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