2015-03-19 33 views
1

我有一個Wrapper<T> where T : class環繞我的對象。我將WeakReference<Wrapper<T>>存儲在ConcurrentDictionary中,爲不可變對象實現弱引用的線程安全緩存,當其他內容需要內存時會自動清理它。我需要在Wrapper析構函數中調用ConcurrentDictionary.TryRemove來釋放字典中不再指向有效對象的弱引用。在析構函數中訪問ConcurrentDictionary

衆所周知,由於死鎖的風險,我們不應該在析構函數內使用任何鎖。所以我想知道,我可以在析構函數中安全地使用ConcurrentDictionary.TryRemove嗎?恐怕它可能使用SpinLock或其他工具來實現,因此在析構函數中使用時仍然存在死鎖的風險。

回答

1

你可以看到ConcurrentDictionary在這個location實施和TryRemove實現使用「鎖(...)」。

你可以在析構函數中做的是使用線程池執行從字典中刪除項目。您仍然需要將包裝程序實例標記爲不再有效,以便如果在終結器運行和線程池之間調用其任何公用方法來將其刪除,您可以檢測到這一點並拒絕該調用。

+0

謝謝,我很害怕這個。你能否提出一些解決方法?一種可能是使用'Interlocked'來設置一個標誌,該標誌需要清除集合,然後在每個公共方法中爲該標誌設置'CompareExchange',但這不是很乾淨的解決方案。我能否使用一些愚蠢的行爲,比如在析構函數中啓動一個線程來清理GC完成後的集合?我不確定直到GC完成或者它們是否同時運行,從析構函數開始的線程是否處於掛起狀態。 – Paya 2015-03-19 04:13:04

+0

更新建議使用線程池來刪除項目。 – 2015-03-19 04:24:57

+0

因此,如果我在線程池('Task.Factory.StartNew',或者除'new Thread()')之外的其他任務上安排任務,那麼線程將在所有垃圾收集完成後開始?我擔心通過在終結器中啓動任何類型的線程,該線程可能與終結線程同時運行。也許這種情況發生,當我在'Wrapper'終結器中時,字典也被垃圾收集,因此,啓動一個新線程來對正在收集的集合進行清理可能不是一個好主意。 – Paya 2015-03-19 04:47:12

0

所以你的原因不要想要在析構函數中使用鎖定是因爲析構函數可能在Final語句線程中由FinalizerWorker調用,同時在所有線程上停止執行。

因此,如果一個線程在FinalizerWorker啓動時處於ConcurrencyDicitonary操作的中間,那麼如果析構函數嘗試鎖定ConcurrencyDictionary(這可能很難重現死鎖),則可能會死鎖。

自旋鎖不會幫助你,因爲如果任何當前正在執行的線程具有ConcurrencyDictionary鎖定或微調可變鎖定它不會釋放,直到FinalizerWorker完成,它不會因爲它會旋轉/永遠鎖定。

您在這裏的主要選項是通過SuppressFinalize(this)調用實現IDisposable接口,因爲您的對象將抑制Finalizer worker,所以不會發生死鎖並且ConcurrencyDictionary操作是安全的!

因此,如果您使用object.Dispose搶先終結(),你應該是安全的使用ConcurrencyDictionary,但在其他方面不要使用任何類型的鎖,你終結的Dispose(假)調用,或者你會在某個時候發生死鎖。

// Design pattern for a base class. 
public class Base: IDisposable 
{ 
    private bool disposed = false; 
//Implement IDisposable. 
public void Dispose() 
{ 
    Dispose(true); 
    GC.SuppressFinalize(this); 
} 

protected virtual void Dispose(bool disposing) 
{ 
    if (!disposed) 
    { 
     //Disposing outside the FinalizerWorker (SAFE) 
     if (disposing)    
      m_pDictionary.TryRemove(this);    

     disposed = true; 
    } 
} 

// Use C# destructor syntax for finalization code. 
~Base() 
{ 
    // Simply call Dispose(false). 
    Dispose (false); 
} 
+0

閱讀問題,問題是'我可以在析構函數中安全地使用ConcurrentDictionary.TryRemove嗎?'。 – 2015-03-19 03:07:44

+0

爲什麼即使嘗試它可以拋出一個在析構函數中是否定的異常。它通常不利於在析構函數中清理外部資源。 IDisposable是有原因的 – 2015-03-19 03:10:54

+0

@GaryKaizer你知道MSDN推薦的'IDisposable'實現模式在析構函數中調用'Dispose'嗎? – Paya 2015-03-19 03:22:41

0

這裏的答案讓你誤入歧途。不鼓勵在析構函數/終結器中使用鎖定,因爲它很容易導致死鎖,尤其是在「手動」實現而不是使用併發集合時,但有時是必要的。即使在「停止世界」時,GC實現的終結器也會在一個單獨的終結器線程上運行,並與您的應用程序同時出現。

首先是第一件事情 - 雖然你非常難以理解你所說的是實現你想要的功能的理想方式,但我相信它並非如此。首先,WeakReferences不適合用於緩存,因爲它們的獲取比「需要內存時」的頻率要多得多。適當的緩存實現通過強引用來監視內存級別,並在內存使用率過高時根據需要進行釋放。

即使在像WeakValueDictionary這樣的實現中,如果可以收集集合,它們不想讓集合保持該值,但實現仍然不會收到對象收集通知。相反,只要您偶然發現收集到的一個條目,或者您每掃描一次X操作或每Y秒等掃描整個集合中的條目,就會刪除該條目。

也就是說,如果遇到了你需要在收集對象時做些事情,你可以使用併發集合。

假設您不會做任何愚蠢的事情,在通知隊列中排隊標識或從併發字典中刪除項目是安全的,因爲這些操作速度很快,並且只會在很短的時間內阻止並且您的應用程序是在終結器運行時不會被阻塞。

這是你應該儘可能避免的東西,但有時它是實現某些東西的最好也是唯一的方法。只要確保鎖定速度快並儘可能少使用,而不是任何多級鎖定方案的一部分,這些鎖定方案很容易意外地出現錯誤和死鎖。