TN058: MFC Modül Durumu Uygulaması
Dekont
Aşağıdaki teknik not, çevrimiçi belgelere ilk kez eklendiğinden beri güncelleştirilmemiştir. Sonuç olarak, bazı yordamlar ve konular güncel olmayabilir veya yanlış olabilir. En son bilgiler için, çevrimiçi belge dizininde ilgilendiğiniz konuyu aramanız önerilir.
Bu teknik not, MFC "modül durumu" yapılarının uygulanmasını açıklar. Modül durumu uygulamasının anlaşılması, DLL'den (veya OLE işlem içi sunucusundan) MFC paylaşılan DLL'lerini kullanmak için kritik öneme sahiptir.
Bu notu okumadan önce, Yeni Belgeler, Windows ve Görünümler Oluşturma başlığı altındaki "MFC Modüllerinin Durum Verilerini Yönetme" konusuna bakın. Bu makale, bu konuyla ilgili önemli kullanım bilgileri ve genel bakış bilgileri içerir.
Genel Bakış
Üç tür MFC durumu bilgisi vardır: Modül Durumu, İşlem Durumu ve İş Parçacığı Durumu. Bazen bu durum türleri birleştirilebilir. Örneğin, MFC'nin tanıtıcı eşlemeleri hem yerel modül hem de iş parçacığı yereldir. Bu, iki farklı modülün iş parçacıklarının her birinde farklı haritalara sahip olmasını sağlar.
İşlem Durumu ve İş Parçacığı Durumu birbirine benzer. Bu veri öğeleri, geleneksel olarak genel değişkenler olan, ancak uygun Win32s desteği veya uygun çoklu iş parçacığı desteği için belirli bir işleme veya iş parçacığına özgü olması gereken öğelerdir. Belirli bir veri öğesinin hangi kategoriye sığdığı, bu öğeye ve işlem ve iş parçacığı sınırlarıyla ilgili olarak istenen semantiğine bağlıdır.
Modül Durumu, gerçekten genel durumu veya yerel veya iş parçacığı yerel olarak işlenen durumu içerebildiği için benzersizdir. Buna ek olarak, hızlı bir şekilde değiştirilebilir.
Modül Durumu Değiştirme
Her iş parçacığı "geçerli" veya "etkin" modül durumuna bir işaretçi içerir (şaşırtıcı olmayan bir şekilde, işaretçi MFC'nin iş parçacığı yerel durumunun bir parçasıdır). Yürütme iş parçacığı ole denetimine veya DLL'ye çağrı yapan bir uygulama veya bir uygulamaya geri çağrı yapan ole denetimi gibi bir modül sınırı geçtiğinde bu işaretçi değiştirilir.
Geçerli modül durumu çağrılarak AfxSetModuleState
değiştirilir. Çoğu zaman API ile doğrudan ilgilenmezsiniz. MFC, çoğu durumda bunu sizin için çağırır (WinMain, OLE giriş noktaları, AfxWndProc
vb.). Bu, özel bir WndProc
öğesine statik olarak bağlanarak ve hangi modül durumunun geçerli olması gerektiğini bilen özel WinMain
(veya DllMain
) bir bileşene bağlanarak yazdığınız tüm bileşenlerde gerçekleştirilir. DLLMODUL'a bakarak bu kodu görebilirsiniz. CPP veya APPMODUL. MFC\SRC dizininde CPP.
Modül durumunu ayarlamak ve sonra geri ayarlamak istemediğiniz nadirdir. Çoğu zaman kendi modül durumunuzu geçerli durum olarak "göndermek" ve işiniz bittikten sonra özgün bağlamı geri "açmak" istersiniz. Bu, makro AFX_MANAGE_STATE ve özel sınıfı AFX_MAINTAIN_STATE
tarafından gerçekleştirilir.
CCmdTarget
modül durumu değiştirmeyi desteklemek için özel özelliklere sahiptir. Özellikle, ole CCmdTarget
otomasyonu ve OLE COM giriş noktaları için kullanılan kök sınıfıdır. Sisteme sunulan diğer giriş noktaları gibi bu giriş noktalarının da doğru modül durumunu ayarlaması gerekir. Verilen CCmdTarget
, "doğru" modül durumunun ne olması gerektiğini nasıl bilir? Bunun yanıtı, "geçerli" modül durumunun oluşturulduğunda ne olduğunu "anımsaması", böylece daha sonra çağrıldığında geçerli modül durumunu bu "hatırlanan" değere ayarlayabilmesidir. Sonuç olarak, belirli CCmdTarget
bir nesnenin ilişkilendirildiği modül durumu, nesne oluşturulduğunda geçerli olan modül durumudur. INPROC sunucusunu yükleme, nesne oluşturma ve yöntemlerini çağırma gibi basit bir örnek alın.
DLL, kullanılarak
LoadLibrary
OLE tarafından yüklenir.RawDllMain
ilk olarak çağrılır. Modül durumunu DLL için bilinen statik modül durumuna ayarlar. Bu nedenleRawDllMain
DLL'ye statik olarak bağlanır.Nesnemizle ilişkili sınıf fabrikasının oluşturucusunun adı verilir.
COleObjectFactory
türetilirCCmdTarget
ve sonuç olarak hangi modül durumunda örneği oluşturulduğunu anımsar. Bu önemlidir; sınıf fabrikasından nesne oluşturması istendiğinde, artık hangi modül durumunun geçerli hale getiriliyor olduğunu bilir.DllGetClassObject
sınıf fabrikasını almak için çağrılır. MFC, bu modülle ilişkili sınıf fabrikası listesini arar ve döndürür.COleObjectFactory::XClassFactory2::CreateInstance
çağrıldığında. Nesneyi oluşturmadan ve döndürmeden önce, bu işlev modül durumunu 3. adımda geçerli olan modül durumuna ayarlar (örneği oluşturulurkenCOleObjectFactory
geçerli olan durum). Bu işlem METHOD_PROLOGUE içinde yapılır.Nesne oluşturulduğunda, bu da bir
CCmdTarget
türevdir ve aynı şekildeCOleObjectFactory
hangi modül durumunun etkin olduğu hatırlanır, bu yeni nesne de öyledir. Artık nesne çağrıldığında hangi modül durumuna geçilir biliyor.İstemci, çağrısından aldığı OLE COM nesnesinde
CoCreateInstance
bir işlev çağırır. Nesne çağrıldığında modül durumunu tıpkı olduğu gibiCOleObjectFactory
değiştirmek için kullanırMETHOD_PROLOGUE
.
Gördüğünüz gibi, modül durumu oluşturulduklarında nesneden nesneye yayılır. Modül durumunun uygun şekilde ayarlanması önemlidir. Ayarlanmadıysa, DLL veya COM nesneniz onu çağıran bir MFC uygulamasıyla kötü etkileşimde bulunabilir veya kendi kaynaklarını bulamayabilir ya da başka sefil yollarla başarısız olabilir.
Belirli tür DLL'lerin, özellikle de "MFC Uzantısı" DLL'lerinin RawDllMain
modül durumunu değiştirmediğini unutmayın (aslında, genellikle bir RawDllMain
. Bunun nedeni, onları kullanan uygulamada gerçekten "sanki" davranmaları gerektiğidir. Bunlar, çalışmakta olan uygulamanın çok önemli bir parçasıdır ve bu uygulamanın genel durumunu değiştirmek onların amacıdır.
OLE Denetimleri ve diğer DLL'ler çok farklıdır. Çağıran uygulamanın durumunu değiştirmek istemiyorlar; onları çağıran uygulama bir MFC uygulaması bile olmayabilir ve bu nedenle değiştirilecek durum olmayabilir. Modül durumu değiştirmenin icat olmasının nedeni budur.
DLL'nizde bir iletişim kutusu başlatan bir DLL'den dışarı aktarılan işlevler için, işlevin başına aşağıdaki kodu eklemeniz gerekir:
AFX_MANAGE_STATE(AfxGetStaticModuleState())
Bu, geçerli modül durumunu, geçerli kapsamın sonuna kadar AfxGetStaticModuleState'ten döndürülen durumla değiştirir.
AFX_MODULE_STATE makro kullanılmazsa DLL'lerdeki kaynaklarla ilgili sorunlar oluşur. Varsayılan olarak MFC, kaynak şablonunu yüklemek için ana uygulamanın kaynak tutamacını kullanır. Bu şablon aslında DLL'de depolanır. Bunun kök nedeni MFC'nin modül durumu bilgilerinin AFX_MODULE_STATE makro tarafından değiştirilmemiş olmasıdır. Kaynak tanıtıcısı MFC'nin modül durumundan kurtarılır. Modül durumunun değiştirilmemesi yanlış kaynak tanıtıcısının kullanılmasına neden olur.
AFX_MODULE_STATE DLL'deki her işleve konulması gerekmez. Örneğin, InitInstance
MFC modül durumunu önceki modül durumunu InitInstance
otomatik olarak kaydırdığından ve döndürdüğünde InitInstance
geri döndürdüğünden, AFX_MODULE_STATE olmadan uygulamadaki MFC kodu tarafından çağrılabilir. Aynı durum tüm ileti eşleme işleyicileri için de geçerlidir. Normal MFC DLL'leri aslında herhangi bir iletiyi yönlendirmeden önce modül durumunu otomatik olarak değiştirir özel bir ana pencere yordamına sahiptir.
Yerel Verileri İşleme
Win32s DLL modelinin zorluğu olmasaydı, yerel verilerin işlenmesi bu kadar önemli olmazdı. Win32'lerde tüm DLL'ler, birden çok uygulama tarafından yüklendiğinde bile genel verilerini paylaşır. Bu, "gerçek" Win32 DLL veri modelinden çok farklıdır ve burada dll'ye eklenen her işlemde her DLL veri alanının ayrı bir kopyasını alır. Karmaşıklık düzeyine eklemek için, Win32s DLL'sindeki yığında ayrılan veriler aslında işleme özgüdür (en azından sahipliğe göre). Aşağıdaki verileri ve kodu göz önünde bulundurun:
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);
}
Yukarıdaki kod bir DLL'de bulunuyorsa ve DLL iki A ve B işlemi tarafından yüklenirse ne olacağını düşünün (aslında, aynı uygulamanın iki örneği olabilir). Çağrısı SetGlobalString("Hello from A")
. Sonuç olarak, A işlemi bağlamında veriler için CString
bellek ayrılır. Kendisinin genel olduğunu CString
ve hem A hem de B tarafından görüldüğünü unutmayın. Şimdi B, öğesini çağırır GetGlobalString(sz, sizeof(sz))
. B, A'nın ayarlandığı verileri görebilir. Bunun nedeni Win32s'nin Win32 gibi işlemler arasında koruma sunmamasıdır. İlk sorun bu; çoğu durumda, tek bir uygulamanın farklı bir uygulamaya ait olduğu kabul edilen genel verileri etkilemesi istenmez.
Ek sorunlar da vardır. Şimdi A'dan çıkıldığını düşünelim. A'dan çıkıldığında, 'strGlobal
' dizesi tarafından kullanılan bellek sistem için kullanılabilir hale getirilir; diğer bir deyişle, A işlemi tarafından ayrılan tüm bellek işletim sistemi tarafından otomatik olarak boşaltılır. Yok edici çağrıldığından CString
serbest değildir; henüz çağrılmamıştır. Yalnızca onu ayıran uygulama sahneyi terk ettiği için serbest bırakılır. Artık B çağrılırsa GetGlobalString(sz, sizeof(sz))
geçerli veri alamayabilir. Başka bir uygulama bu belleği başka bir şey için kullanmış olabilir.
Açıkça bir sorun var. MFC 3.x, iş parçacığı yerel depolama (TLS) adlı bir teknik kullandı. MFC 3.x, Win32s altında gerçekten işlem yerel depolama dizini işlevi gören bir TLS dizini ayırır, ancak bu dizin bu şekilde adlandırılmaz ve ardından bu TLS dizinine göre tüm verilere başvurur. Bu, Win32'de iş parçacığı yerel verilerini depolamak için kullanılan TLS dizinine benzer (bu konuda daha fazla bilgi için aşağıya bakın). Bu, her MFC DLL'nin işlem başına en az iki TLS dizini kullanmasına neden oldu. Birçok OLE Denetimi DLL'sinin (OCX) yüklenmesini hesaba kattığınızda, TLS dizinleri hızla tükenir (yalnızca 64 tane kullanılabilir). Buna ek olarak, MFC'nin tüm bu verileri tek bir yerde, tek bir yapıda yerleştirmesi gerekiyordu. Çok genişletilebilir değildi ve TLS dizinlerinin kullanımıyla ilgili olarak ideal değildi.
MFC 4.x bunu yerel olarak işlenmesi gereken verilerin etrafında "sarmalayabileceğiniz" bir dizi sınıf şablonuyla giderir. Örneğin, yukarıda bahsedilen sorun şu yazıyla düzeltilebilir:
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 bunu iki adımda uygular. İlk olarak, kaç DLL'niz olursa olsun işlem başına yalnızca iki TLS dizini kullanan Win32 Tls* API'lerinin (TlsAlloc, TlsSetValue, TlsGetValue vb.) üzerinde bir katman vardır. İkinci olarak CProcessLocal
, bu verilere erişmek için şablon sağlanır. Yukarıda gördüğünüz sezgisel söz dizimine izin veren işleci> geçersiz kılar. tarafından CProcessLocal
sarmalanan tüm nesneler' den CNoTrackObject
türetilmelidir. CNoTrackObject
daha düşük düzeyli bir ayırıcı (LocalAlloc/LocalFree) ve işlem sonlandırıldığında MFC'nin işlem yerel nesnelerini otomatik olarak yok edebildiği bir sanal yıkıcı sağlar. Ek temizleme gerekiyorsa bu tür nesnelerin özel bir yıkıcısı olabilir. Derleyici katıştırılmış CString
nesneyi yok etmek için varsayılan bir yıkıcı oluşturacağı için yukarıdaki örnekte bir tane gerekmez.
Bu yaklaşımın başka ilginç avantajları da vardır. Tüm CProcessLocal
nesneler otomatik olarak yok edilmekle birlikte, ihtiyaç duyulana kadar da inşa edilmez. CProcessLocal::operator->
ilişkilendirilmiş nesne ilk çağrıldığında örneği oluşturur ve daha önce örnek oluşturmaz. Yukarıdaki örnekte bu, 'strGlobal
' dizesinin ilk kez SetGlobalString
GetGlobalString
veya çağrılana kadar oluşturulacağı anlamına gelir. Bazı durumlarda bu, DLL başlatma süresini azaltmaya yardımcı olabilir.
İş Parçacığı Yerel Verileri
yerel verileri işlemeye benzer şekilde, verilerin belirli bir iş parçacığında yerel olması gerektiğinde iş parçacığı yerel verileri kullanılır. Başka bir ifadeyle, bu verilere erişen her iş parçacığı için verilerin ayrı bir örneğine ihtiyacınız vardır. Bu, kapsamlı eşitleme mekanizmaları yerine birçok kez kullanılabilir. Verilerin birden çok iş parçacığı tarafından paylaşılması gerekmiyorsa, bu tür mekanizmalar pahalı ve gereksiz olabilir. Bir CString
nesnemiz olduğunu varsayalım (yukarıdaki örneğe çok benzer). Bir şablonla CThreadLocal
sarmalayarak iş parçacığını yerel hale getirebiliriz:
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
}
}
}
İki farklı iş parçacığından çağrıldıysa MakeRandomString
, her bir dizeyi diğerini engellemeden farklı şekillerde "karıştırırdı". Bunun nedeni aslında yalnızca bir genel örnek yerine iş parçacığı başına bir strThread
örnek olmasıdır.
Döngü yinelemesi başına bir kez yerine bir kez adresi yakalamak CString
için başvurunun nasıl kullanıldığına dikkat edin. Döngü kodu 'str
' kullanılan her yerde ile threadData->strThread
yazılmış olabilir, ancak kod yürütmede çok daha yavaş olacaktır. Bu tür başvurular döngüler halinde gerçekleştiğinde verilere yönelik bir başvuruyu önbelleğe almak en iyisidir.
Sınıf CThreadLocal
şablonu, aynı mekanizmaları CProcessLocal
ve aynı uygulama tekniklerini kullanır.