共用方式為


TN058:MFC 模組狀態實作

備註

自第一次包含在在線文件中以來,尚未更新下列技術附注。 因此,某些程式和主題可能已過期或不正確。 如需最新信息,建議您搜尋在線檔索引中感興趣的主題。

此技術附注描述 MFC「模組狀態」建構的實作。 瞭解模組狀態的實作方式對於在 DLL 中使用 MFC 共用 DLL(或 OLE 進程內伺服器)而言非常重要。

閱讀此附注之前,請參閱 建立新檔、Windows 和檢視中的<管理 MFC 模組的狀態數據>。 本文包含此主題的重要使用資訊和概觀資訊。

概觀

MFC 狀態資訊有三種:模組狀態、進程狀態和線程狀態。 有時候可以合併這些狀態類型。 例如,MFC 的句柄對應既是模組區域,也 是執行緒區域。 這可讓兩個不同的模組在每個執行緒中擁有不同的映射。

進程狀態和線程狀態很類似。 這些數據項傳統上是用作全域變數的項目,但需要針對特定的進程或線程進行限定,以便於提供正確的 Win32s 支援或多線程支援。 特定數據項目適合的類別取決於該數據項目及其所需的語義,相对于流程和線程邊界。

模組狀態具有獨特性,因為它可以包含真正的全域狀態,或是進程本地或執行緒本地的狀態。 此外,也可以快速切換。

模組狀態切換

每個線程都包含「目前」或「使用中」模組狀態的指標(毫不奇怪,指標是 MFC 線程本機狀態的一部分)。 當執行緒通過模組邊界時,此指標會變更,例如應用程式呼叫 OLE 控件或 DLL,或者 OLE 控件回呼到應用程式。

目前的模組狀態會藉由呼叫 AfxSetModuleState來切換。 在大多數情況下,您永遠不會直接處理 API。 在許多情況下,MFC 會為您調用它(如在 WinMain、OLE 入口點、AfxWndProc 等)。 這可在您撰寫的任何元件中完成,方法是靜態地連結進特殊的 WndProc,以及能夠識別應該使用哪個模塊狀態的特殊 WinMain (或 DllMain)。 您可以查看 MFC\SRC 目錄中的 DLLMODUL.CPP 或 APPMODUL.CPP 來檢視此程式碼。

您很少會想設定模組狀態後不將它設回原狀。 大部分時候,您會想要先將自己的模組狀態「推送」成為當前狀態,完成後再將原始情境「彈回」來。 這是由巨集 AFX_MANAGE_STATE 和特殊類別 AFX_MAINTAIN_STATE來完成。

CCmdTarget 具有支援模組狀態切換的特殊功能。 特別是, CCmdTarget 是用於 OLE 自動化和 OLE COM 進入點的根類別。 如同向系統公開的任何其他進入點,這些進入點必須設定正確的模塊狀態。 指定 CCmdTarget 如何知道「正確」模組狀態應該是「答案」是,它會在建構時「記住」「目前」模組狀態是什麼,如此一來,它就可以在稍後呼叫時,將目前的模塊狀態設定為該「記住」值。 因此,與指定 CCmdTarget 對象相關聯的模組狀態是建構物件時目前狀態的模塊狀態。 以載入 INPROC 伺服器、建立物件及呼叫其方法的簡單範例為例。

  1. OLE 使用 LoadLibrary 載入 DLL。

  2. RawDllMain 會被先呼叫。 它會將模組狀態設定為 DLL 的已知靜態模組狀態。 因此, RawDllMain 靜態連結至 DLL。

  3. 呼叫與對象相關聯的類別處理站建構函式。 COleObjectFactory 衍生自 CCmdTarget ,因此會記住其具現化模組狀態。 這很重要—當類別工廠被要求建立物件時,它現在知道要把哪個模塊狀態設為當前的。

  4. DllGetClassObject 被呼叫以獲得類別工廠。 MFC 會搜尋與此模組相關聯的類別工廠清單,並傳回該清單。

  5. 呼叫 COleObjectFactory::XClassFactory2::CreateInstance。 建立物件並傳回物件之前,此函式會將模塊狀態設定為步驟 3 中目前的模塊狀態(具現化 時 COleObjectFactory 目前的模組狀態)。 這是在 METHOD_PROLOGUE內完成的。

  6. 建立物件時,它也是 CCmdTarget 的衍生物件,並且以相同的方式會記住哪些模組狀態處於使用中,因此這個新物件也是如此。 現在物件知道每當呼叫模組時要切換至哪個模塊狀態。

  7. 用戶端會在其 CoCreateInstance 呼叫所收到的 OLE COM 物件上呼叫函式。 呼叫 物件時,它會使用 METHOD_PROLOGUE 來切換模組狀態,就像 COleObjectFactory 這樣。

如您所見,模組狀態會在建立物件時從 對象傳播到物件。 請務必適當地設定模塊狀態。 如果未設定,您的 DLL 或 COM 物件可能會與呼叫它的 MFC 應用程式互動不佳,或可能找不到自己的資源,或可能在其他錯誤的方式失敗。

請注意,某些類型的 DLL,特別是 “MFC 擴充功能” DLL,不會在 RawDllMain 中切換模組狀態(實際上,它們通常甚至沒有 RawDllMain)。 因為它們的目的是讓人感覺就好像它們實際上存在於使用它們的應用程式中。 它們是正在執行的應用程式的重要部分,而且它們意圖修改該應用程式的全域狀態。

OLE 控制件和其他 DLL 非常不同。 他們不想修改呼叫端應用程式的狀態;呼叫它們的應用程式甚至可能不是 MFC 應用程式,因此可能沒有任何狀態可修改。 這是模組狀態切換發明的原因。

針對從 DLL 導出的函式,例如在 DLL 中啟動對話框的函式,您需要將下列程式代碼新增至函式的開頭:

AFX_MANAGE_STATE(AfxGetStaticModuleState())

這會將目前的模組狀態與從 AfxGetStaticModuleState 傳回的狀態交換,直到目前範圍的結尾。

如果未使用AFX_MODULE_STATE巨集,DLL 中的資源就會發生問題。 根據預設,MFC 會使用主要應用程式的資源句柄來載入資源範本。 此範本實際上會儲存在 DLL 中。 根本原因是 MFC 的模組狀態資訊尚未由 AFX_MODULE_STATE 巨集切換。 資源控制代碼是從 MFC 的模組狀態中恢復的。 未切換模組狀態會導致使用錯誤的資源句柄。

AFX_MODULE_STATE不需要放入 DLL 中的每個函式中。 例如,在應用程式中,您可以由 MFC 程式碼呼叫 InitInstance,而不需要 AFX_MODULE_STATE,因為 MFC 會在 InitInstance 之前自動變更模組狀態,然後在 InitInstance 傳回後恢復原狀。 所有訊息對應處理程式也是如此。 一般的 MFC DLL 實際上有一個特殊的主視窗程序,它在路由任何訊息前會自動切換模組狀態。

處理本機數據

若不是因為 Win32s DLL 模型的困難,處理本機數據就不會讓人如此擔心。 在 Win32 中,所有 DLL 都會共用其全域數據,即使由多個應用程式載入也是如此。 這與「實際」Win32 DLL 數據模型非常不同,其中每個 DLL 會在附加至 DLL 的每個進程中取得其數據空間的個別複本。 為了增加其複雜性,Win32s DLL 中在堆疊上分配的資料實際上是特定於程序的(至少就擁有權而言)。 請考慮下列資料和程式代碼:

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);
}

如果上述程式代碼位於 DLL 中,而且 DLL 是由兩個進程 A 和 B 載入,會發生什麼情況(事實上,可能是相同應用程式的兩個實例)。 A 打電話給 SetGlobalString("Hello from A")。 因此,記憶體會在進程 A 的情境中配置給 CString 數據。請記住,CString 本身是全域的,並且對 A 和 B 都是可見的。現在 B 呼叫 GetGlobalString(sz, sizeof(sz))。 B 將可以看到 A 設定的數據。 這是因為 Win32s 不像 Win32 那樣在程序之間提供保護。 這是第一個問題:在許多情況下,不想要讓一個應用程式影響被視為由不同應用程式所擁有的全域數據。

還有其他問題。 假設 A 現在離開。 當 A 結束時,『strGlobal' 字串所使用的記憶體可供系統使用,也就是說,由進程 A 配置的所有記憶體都會由作系統自動釋放。 它未釋放,是因為尚未呼叫 CString 解構函式。 它只是因為配置它的應用程式已經退出而被釋放。 現在,如果呼叫 GetGlobalString(sz, sizeof(sz))B,它可能無法取得有效的數據。 其他一些應用程式可能已使用該記憶體做為其他專案。

顯然存在問題。 MFC 3.x 使用了稱為線程本機記憶體 (TLS) 的技術。 MFC 3.x 會配置 TLS 索引,在 Win32s 下,它實際上會當做進程區域存儲索引,儘管它不叫這個名稱,然後會根據該 TLS 索引引用所有數據。 這類似於用來在 Win32 上儲存線程本機數據的 TLS 索引(如需該主題的詳細資訊,請參閱下文)。 這會導致每個 MFC DLL 在每個進程中至少使用兩個 TLS 索引。 當您考慮載入許多 OLE 控制 DLL (OCX)時,您很快就用完 TLS 索引(只有 64 個可用)。 此外,MFC 必須將所有這些數據放在單一結構中一個位置。 它延展性不佳,而且在使用 TLS 索引方面並不理想。

MFC 4.x 會使用一組類別模板來解決這個問題,您可以「包裝」在應由進程本地處理的數據上。 例如,上述問題可藉由撰寫來修正:

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 會在兩個步驟中實作此動作。 首先,在 Win32 Tls* API 的頂端有一層(TlsAllocTlsSetValueTlsGetValue 等),不論您有多少 DLL,每個進程只能使用兩個 TLS 索引。 其次,範本 CProcessLocal 被提供用來存取此數據。 它會覆寫運算符>,這正是讓您看到上述直覺易懂語法的原因。 被CProcessLocal包裝的所有物件都必須衍生自CNoTrackObjectCNoTrackObject 提供較低層級的配置器 (LocalAlloc/LocalFree) 和虛擬解構函式,讓 MFC 可以在進程終止時自動終結進程本機物件。 若需要額外的清除,這類物件可被設計成擁有自定義的解構函式。 上述範例不需要一個,因為編譯程式會產生默認解構函式來終結內嵌 CString 物件。

此方法還有其他有趣的優點。 這些對象不僅 CProcessLocal 會自動銷毀,而且只有在需要時才會建構。 CProcessLocal::operator-> 會在第一次呼叫關聯物件時具現化關聯物件,而且不會更快。 在上述範例中,這意味著 'strGlobal' 字串將在第一次呼叫 SetGlobalStringGetGlobalString 才會建構。 在某些情況下,這有助於減少 DLL 啟動時間。

執行緒本地數據

與處理程序本地數據相似,當數據必須是特定線程的本地數據時,就會使用線程本地數據。 也就是說,您需要針對每個存取該資料的線程,擁有各自獨立的資料實例。 這可以多次用於取代廣泛的同步處理機制。 如果數據不需要由多個線程共用,這類機制可能相當昂貴且不必要。 假設我們有物件 CString (就像上述範例一樣)。 我們可以將其用 CThreadLocal 範本包裝,以使其成為線程本地變數。

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
        }
    }
}

如果從兩個不同的線程呼叫 MakeRandomString,每個線程都會以不同的方式洗牌該字串,而不會干擾另一個線程。 每個線程實際上都有一個 strThread 實例,而不是只有一個全域實例。

請注意如何僅使用一次參考來擷取 CString 位址,而不是在每次迴圈反覆時都執行一次。 迴圈程式碼本可以在所有使用「threadData->strThread」的地方使用str 來撰寫,但這樣程式的執行速度會明顯變慢。 在參考出現在迴圈中時,最好緩存該數據的參考。

類別 CThreadLocal 範本會使用相同的機制 CProcessLocal ,以及相同的實作技術。

另請參閱

依編號的技術注意事項
依類別排序的技術注意事項