簡體   English   中英

C ++ / CLI庫中的早期完成和內存泄漏

[英]Early finalization and memory leaks in C++/CLI library

我遇到的問題似乎是在我正在研究的C ++ / CLI(和C#)項目中早期調用終結器。 這似乎是一個非常復雜的問題,我將從代碼中提到很多不同的類和類型。 幸運的是它是開源的,你可以在這里繼續: Pstsdk.Net (mercurial repository)我也嘗試在適當的時候直接鏈接到文件瀏覽器,這樣你就可以在閱讀時查看代碼。 我們處理的大多數代碼都在存儲庫的pstsdk.mcpp文件夾中。

現在的代碼處於相當可怕的狀態(我正在研究它),我正在處理的代碼的當前版本是在Finalization fixes (UNSTABLE!)分支中。 該分支中有兩個變更集,為了理解我的冗長問題,我們需要同時處理這兩個變更集。 (變更集: ee6a002df36fa12e9f5ea9fe

在某些背景下,該項目是用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_由於終結器運行,指針被重置。

1) 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幾乎一無所知之外,我無法將本機類放在托管代碼中。 我覺得有可能使用范圍指針,當類不再使用時會自動釋放內存,但我不能確定這是否是一種有效的方法來解決這個問題,或者它是否可行。 所以,我的第二個問題是:

2)以無痛方式處理托管類中非托管資源的最佳方法是什么?

詳細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.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM