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 AfxSetModuleState
gewechselt. 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, AfxWndProc
usw.). Dies erfolgt in jeder Komponente, die Sie schreiben, indem Sie statisch eine Verknüpfung in einem speziellen Element erstellen, und einem speziellen WndProc
WinMain
(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.
Die DLL wird mithilfe von OLE
LoadLibrary
geladen.RawDllMain
wird zuerst aufgerufen. Er legt den Modulstatus auf den bekannten statischen Modulzustand für die DLL fest. Aus diesem GrundRawDllMain
ist statisch mit der DLL verknüpft.Der Konstruktor für die Klassenfactory, die unserem Objekt zugeordnet ist, wird aufgerufen.
COleObjectFactory
wird vonCCmdTarget
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.DllGetClassObject
wird aufgerufen, um die Klassenfactory abzurufen. MFC durchsucht die diesem Modul zugeordnete Klassenfactoryliste und gibt sie zurück.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 beimCOleObjectFactory
Instanziieren des Objekts aktuell war). Dies geschieht innerhalb von METHOD_PROLOGUE.Wenn das Objekt erstellt wird, handelt es sich ebenfalls um ein
CCmdTarget
Abgeleitetes und auf die gleiche WeiseCOleObjectFactory
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.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 erMETHOD_PROLOGUE
verwendet, um den Modulzustand wieCOleObjectFactory
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
. CNoTrackObject
stellt 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