[英]Memory leaks in C# while using C++/CLI defined class with finalizer
[英]Early finalization and memory leaks in C++/CLI library
我遇到的問題似乎是在我正在研究的C ++ / CLI(和C#)項目中早期調用終結器。 這似乎是一個非常復雜的問題,我將從代碼中提到很多不同的類和類型。 幸運的是它是開源的,你可以在這里繼續: Pstsdk.Net (mercurial repository)我也嘗試在適當的時候直接鏈接到文件瀏覽器,這樣你就可以在閱讀時查看代碼。 我們處理的大多數代碼都在存儲庫的pstsdk.mcpp
文件夾中。
現在的代碼處於相當可怕的狀態(我正在研究它),我正在處理的代碼的當前版本是在Finalization fixes (UNSTABLE!)
分支中。 該分支中有兩個變更集,為了理解我的冗長問題,我們需要同時處理這兩個變更集。 (變更集: ee6a002df36f和a12e9f5ea9fe )
在某些背景下,該項目是用C ++編寫的非托管庫的C ++ / CLI包裝器。 我不是該項目的協調員,有幾個我不同意的設計決策,因為我相信很多看過這些代碼的人會,但我離題了。 我們在C ++ / CLI dll中包含了大部分原始庫層,但是在C#dll中展示了易於使用的API。 這樣做是因為項目的目的是將整個庫轉換為托管C#代碼。
如果您能夠獲取要編譯的代碼,則可以使用此測試代碼重現該問題。
最新的變更集,題為moved resource management code to finalizers, to show bug
,顯示了我遇到的原始問題。 此代碼中的每個類都使用相同的模式來釋放非托管資源。 這是一個例子(C ++ / CLI):
DBContext::~DBContext()
{
this->!DBContext();
GC::SuppressFinalize(this);
}
DBContext::!DBContext()
{
if(_pst.get() != nullptr)
_pst.reset(); // _pst is a clr_scoped_ptr (managed type)
// that wraps a shared_ptr<T>.
}
此代碼有兩個好處。 首先,當像這樣的類在using
語句中時,資源會立即正確釋放。 其次,如果用戶忘記了處理,當GC最終決定完成該類時,將釋放非托管資源。
這是這種方法的問題,我根本無法理解,有時,GC會決定最終確定一些用於枚舉文件中數據的類。 許多不同的PST文件都會發生這種情況,並且我已經能夠確定它與被調用的Finalize方法有關,即使該類仍在使用中。
我可以用這個文件(下載) 1來實現它。 早期調用的終結器位於DBAccessor.cpp文件中的NodeIdCollection
類中。 如果您能夠運行上面鏈接的代碼(由於對boost庫的依賴性,此項目很難設置),應用程序將因異常而失敗,因為_nodes
列表設置為null並且_db_
由於終結器運行,指針被重置。
NodeIdCollection
類中的枚舉代碼是否有任何明顯的問題會導致GC在仍在使用時完成此類? 我只能通過下面描述的解決方法使代碼正常運行。
現在,我能夠通過將每個終結器( !classname
)中的所有資源管理代碼移動到析構函數( ~classname
)來解決此問題。 這已經解決了這個問題,雖然它沒有解決我為什么要盡早完成課程的好奇心。
但是,這種方法存在問題,我承認這對設計來說更是個問題。 由於代碼中指針的大量使用,幾乎每個類都處理自己的資源,並且需要處理每個類。 這使得使用枚舉非常難看(C#):
foreach (var msg in pst.Messages)
{
// If this using statement were removed, we would have
// memory leaks
using (msg)
{
// code here
}
}
作用於集合中的項目的using語句對我來說是錯誤的,但是,使用這種方法非常有必要防止任何內存泄漏。 沒有它,即使調用了pst類上的dispose方法,也永遠不會調用dispose並且永遠不會釋放內存。
我有意嘗試改變這種設計。 這個代碼第一次編寫時的基本問題,除了我對C ++ / CLI幾乎一無所知之外,我無法將本機類放在托管代碼中。 我覺得有可能使用范圍指針,當類不再使用時會自動釋放內存,但我不能確定這是否是一種有效的方法來解決這個問題,或者它是否可行。 所以,我的第二個問題是:
詳細clr_scoped_ptr
,我可以用最近添加到代碼中的clr_scoped_ptr
包裝器替換本機指針(來自此 stackexchange問題的clr_scoped_ptr.h )。 或者我需要將本機指針包裝在scoped_ptr<T>
或smart_ptr<T>
?
感謝您閱讀所有這些,我知道這很多。 我希望我已經足夠清楚,以便我可以從比我更有經驗的人那里得到一些見解。 這是一個很大的問題,我打算在它允許的時候添加賞金。 希望有人可以提供幫助。
謝謝!
1此文件是PST文件的免費安全數據集的一部分
clr_scoped_ptr
是我的,來自這里 。
如果有任何錯誤,請告訴我。
即使我的代碼不完美,使用智能指針也是解決此問題的正確方法,即使在托管代碼中也是如此。
您不需要(也不應該)在終結器中重置clr_scoped_ptr
。 每個clr_scoped_ptr
本身都將由運行時完成。
使用智能指針時,您不需要編寫自己的析構函數或終結器。 編譯器生成的析構函數將自動調用所有子對象上的析構函數,並且每個子對象終結器將在收集時運行。
仔細觀察您的代碼, NodeIdCollection
確實存在錯誤。 GetEnumerator()
每次調用時都必須返回一個不同的枚舉器對象,以便每個枚舉都從序列的開頭開始。 您正在重復使用單個枚舉器,這意味着在連續調用GetEnumerator()
之間共享該位置。 那很糟。
我想,從一些Microsoft文檔中刷新我對destructors / finalalisers的記憶,你至少可以簡化你的代碼。
這是我的序列版本:
DBContext::~DBContext()
{
this->!DBContext();
}
DBContext::!DBContext()
{
delete _pst;
_pst = NULL;
}
“GC :: SupressFinalize”由C ++ / CLI自動完成,因此不需要。 由於_pst變量在構造函數中初始化(並且刪除空變量無論如何都不會引起任何問題),因此我看不出有任何理由使用智能指針使代碼復雜化。
在調試說明中,我想知道你是否可以通過向“GC :: Collect”調用一些來幫助解決問題。 這應該迫使你完成懸掛物體。
希望這有所幫助,
聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.