2010-06-27 66 views
19

我有一個List集合,我想在一個多線程應用程序中迭代它。每次迭代時我都需要保護它,因爲它可能會被更改,並且我不希望「收集被修改」異常,當我做一個foreach時。Lock與ToArray爲線程安全foreach訪問List集合

這樣做的正確方法是什麼?

  1. 每次訪問或循環時都使用鎖定。我很害怕僵局。也許我只是使用鎖的偏執,不應該。我需要知道我是否走這條路線以避免死鎖?鎖是否相當有效?使用列表<> .ToArray()在每次執行foreach時都複製到數組。這會導致性能下降,但很容易。我擔心內存抖動以及複製它的時間。看起來過度。使用ToArray是否安全?

  2. 不要使用foreach而是使用for循環代替。每次我這樣做以確保列表不縮小時,我是不是需要進行長度檢查?這似乎很煩人。

+0

列出可能是最線程安全的數據結構有! – Egon 2010-06-29 01:12:57

+1

您可以使用[CHESS - 自動化併發測試工具](http://research.microsoft.com/en-us/projects/chess/)和[MSDN](http://channel9.msdn)測試您的假設。 com/shows/Going + Deep/CHESS-An-Automated-Concurrency-Testing-Tool /) – 2010-06-29 01:23:25

回答

40

害怕死鎖沒什麼理由,它們很容易被發現。你的程序停止運行,死亡贈品。你真的應該感到恐懼的是線程競賽,即當你不應該鎖定的時候你會得到的那種bug。 很難診斷。

  1. 使用鎖定很好,只要確保在任何接觸該列表的代碼中使用完全相同的鎖定對象。就像添加或刪除列表中的項目的代碼一樣。如果該代碼在迭代列表的同一個線程上運行,則不需要鎖定。一般來說,這裏唯一的死鎖機會是如果你的代碼依賴於線程狀態,比如Thread.Join(),而它也持有該鎖定對象。這應該是罕見的。

  2. 是的,只要您在ToArray()方法周圍使用鎖,迭代列表副本始終是線程安全的。請注意,您仍需要鎖定,無需進行結構改進。優點是你可以在很短的時間內保持鎖定,從而提高程序的併發性。缺點是它的O(n)存儲需求,只有一個安全的列表,但沒有保護列表中的元素,並且總是有一個陳舊內容的陳舊觀點的棘手問題。尤其是最後一個問題微妙而難以分析。如果你不能推斷出副作用,那麼你可能不應該考慮這一點。

  3. 確保把foreach作爲禮物來檢測種族的能力,而不是問題。是的,明確的(;;)循環不會拋出異常,它只會發生故障。像迭代同一個項目兩次或完全跳過一個項目。您可以避免通過向後迭代來重新檢查項目數量。只要其他線程只調用Add()而不是Remove(),它們的行爲與ToArray()類似,您將得到陳舊的視圖。並不是說這會在實踐中起作用,索引列表也不是線程安全的。列表<>將根據需要重新分配其內部數組。這不會以不可預知的方式工作和故障。

這裏有兩個觀點。你會感到害怕,並且遵循普通的智慧,你會得到一個可行的程序,但可能不是最佳的。這是明智的,並保持老闆高興。或者你可以嘗試一下,找出自己是如何扭曲規則,讓你陷入困境的。這會讓你開心,你會成爲一個更好的程序員。但是你的生產力將會受到影響。我不知道你的日程安排是什麼樣子。

3

使用lock()除非您有其他理由進行復印。

主題1:

lock(A) { 
    // .. stuff 
    // Next lock request can potentially deadlock with 2 
    lock(B) { 
    // ... more stuff 
    } 
} 

線程2:如果要求在不同的順序多個鎖,例如死鎖只能發生

lock(B) { 
    // Different stuff 
    // next lock request can potentially deadlock with 1 
    lock(A) { 
    // More crap 
    } 
} 

這裏線程1和線程2有潛力導致死鎖,因爲線程1可能持有A而線程2持有B並且兩者都不能繼續,直到其他人釋放其鎖定。

如果您必須採用多個鎖,請始終按照相同的順序進行操作。如果你只取一個鎖,那麼你不會造成死鎖......除非你在等待用戶輸入時持有一個鎖,但這在技術上不是一個死鎖並導致另一個問題:永遠不要再鎖定一個鎖比你絕對必須的。

4

一般而言,除哈希表之外,集合對於性能原因而言不是線程安全的。您必須使用IsSynchronized和SyncRoot來使它們安全。見here並從MSDN here

ICollection myCollection = someCollection; 
lock(myCollection.SyncRoot) 
{ 
    foreach (object item in myCollection) 
    { 
     // Insert your code here. 
    } 
} 

編輯:如果您使用的是.NET 4.0中,你可以使用concurrent collections

10

如果您的列表數據主要是隻讀的,你可以允許多個線程安全地同時訪問它使用ReaderWriterLockSlim

您可以在這裏找到一個Thread-Safe dictionary的實現,以幫助您入門。

我也想提一提,如果您使用.Net 4.0,BlockingCollection類會自動實現此功能。 我希望我會在幾個月前知道

+2

ReaderWriterLockSlim是一個很好的選擇。它將允許多個線程同時枚舉集合,而不像一個只允許一個線程的簡單鎖。它也不會產生ToArray()所涉及的開銷。 – 2010-06-27 22:38:57

4

您也可以考慮使用不可變的數據結構 - 將您的列表視爲值類型。

如果可能的話,使用Immutable對象可以是多線程編程的絕佳選擇,因爲它們可以移除所有笨重的鎖定語義。基本上,任何會改變對象狀態的操作都會創建一個全新的對象。

例如我鞭撻了以下來展示這個想法。我會道歉,這絕不是參考代碼,它開始有點長。

public class ImmutableWidgetList : IEnumerable<Widget> 
{ 
    private List<Widget> _widgets; // we never modify the list 

    // creates an empty list 
    public ImmutableWidgetList() 
    { 
     _widgets = new List<Widget>(); 
    } 

    // creates a list from an enumerator 
    public ImmutableWidgetList(IEnumerable<Widget> widgetList) 
    { 
     _widgets = new List<Widget>(widgetList); 
    } 

    // add a single item 
    public ImmutableWidgetList Add(Widget widget) 
    { 
     List<Widget> newList = new List<Widget>(_widgets); 

     ImmutableWidgetList result = new ImmutableWidgetList(); 
     result._widgets = newList; 
     return result; 
    } 

    // add a range of items. 
    public ImmutableWidgetList AddRange(IEnumerable<Widget> widgets) 
    { 
     List<Widget> newList = new List<Widget>(_widgets); 
     newList.AddRange(widgets); 

     ImmutableWidgetList result = new ImmutableWidgetList(); 
     result._widgets = newList; 
     return result; 
    } 

    // implement IEnumerable<Widget> 
    IEnumerator<Widget> IEnumerable<Widget>.GetEnumerator() 
    { 
     return _widgets.GetEnumerator(); 
    } 


    // implement IEnumerable 
    System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator() 
    { 
     return _widgets.GetEnumerator(); 
    } 
} 
  • 我包括IEnumerable<T>以允許foreach實現。
  • 你提到你擔心創建新列表的空間/時間表現,所以也許這對你不起作用。
  • 您可能還需要實現IList<T>