2012-02-01 113 views
13

考慮一個簡單的Registry類被多個線程訪問方法使用鎖時:LINQ延遲執行中返回IEnumerable的

public class Registry 
{ 
    protected readonly Dictionary<int, string> _items = new Dictionary<int, string>(); 
    protected readonly object _lock = new object(); 

    public void Register(int id, string val) 
    { 
     lock(_lock) 
     { 
      _items.Add(id, val); 
     } 
    } 

    public IEnumerable<int> Ids 
    { 
     get 
     { 
      lock (_lock) 
      { 
       return _items.Keys; 
      } 
     } 
    } 
} 

和典型用法:

var ids1 = _registry.Ids;//execution deferred until line below 
var ids2 = ids1.Select(p => p).ToArray(); 

這個類不是線程安全的,因爲它是可能收到System.InvalidOperationException

收藏被修改;枚舉操作可能不會執行。

時IDS2分配如果作爲_items.Keys執行不鎖下進行的線程調用Register

這可以通過修改Ids返回一個IList予以糾正:

public IList<int> Ids 
    { 
     get 
     { 
      lock (_lock) 
      { 
       return _items.Keys.ToList(); 
      } 
     } 
    } 

但你失去了很多的延遲執行的「善」的,例如

var ids = _registry.Ids.First(); //much slower! 

所以,
1)在這種特殊情況下,是否有任何線程安全的選項,涉及IEnumerable
2)什麼是一些最佳實踐時wo與IEnumerable和鎖?

+3

問題同時使用不返回值的屬性ID,它指的是Dictionary.Keys,它是一個共享實例(對同一字典實例的每次調用都返回相同的密鑰集合實例)。或者像第二個實現那樣創建一個副本,或者使用ConcurrentDictionary類 – Polity 2012-02-01 13:15:12

+4

只需使用'ConcurrentDictionary ',然後轉儲鎖定:http://msdn.microsoft.com/en-us/library/dd287191.aspx – LukeH 2012-02-01 13:15:35

+1

請注意,您在你的第一個片段中重新查看字典的關鍵集合,這不是一個流式序列。這與你的List版本在這方面沒有多大區別。 – 2012-02-01 13:17:56

回答

9

當訪問您的Ids屬性,則該詞典不能被更新,但沒有什麼可以從在同一時間爲IEnumerator<int>Ids得到了LINQ延遲執行更新停止字典。

Ids屬性內調用.ToArray().ToList(),並且鎖內部將消除此處的線程問題,只要字典的更新也被鎖定。不鎖定字典和ToArray()的更新,它仍然可能導致競爭條件,因爲內部.ToArray().ToList()對IEnumerable進行操作。

爲了解決這個問題,你需要在鎖內取得ToArray的性能命中率,並鎖定你的字典更新,或者你可以創建一個自己的線程安全的自定義IEnumerator<int>。只有通過控制迭代(並在該點鎖定),或者通過鎖定數組副本,才能實現這一點。

1

如果使用yield return內的財產,那麼編譯器將確保鎖被採取的第一次調用MoveNext()

一些例子可以在下面找到,並在統計員被處置時被釋放。

但是,我會避免使用這個,因爲執行不當的調用者代碼可能會忘記調用Dispose,從而造成死鎖。

public IEnumerable<int> Ids  
{ 
    get  
    { 
     lock (_lock)    
     { 
      // compiler wraps this into a disposable class where 
      // Monitor.Enter is called inside `MoveNext`, 
      // and Monitor.Exit is called inside `Dispose` 

      foreach (var item in _items.Keys) 
       yield return item; 
     }   
    }  
} 
+2

我不相信它的確如此。我剛剛測試了這一點,它也失敗了。在上面的例子中,只有在分配'ids2'時纔會命中收益率回報,即在鎖外 – wal 2012-02-01 13:32:21

+0

@Groo no時,只要退出方法,它就會釋放鎖。該方法本身只是返回一個新的枚舉。 – Polity 2012-02-01 13:47:41

+0

@Groo:編譯器*不會釋放每個yield之間的鎖。它只是在調用枚舉器對象的第一個'MoveNext'調用之前不需要鎖定。這本身導致了這個代碼的另一個問題:一個惡意和/或愚蠢的調用者可能會做'Ids.GetEnumerator()。MoveNext()',然後該鎖被永久保存;該對象變得不可用。 – LukeH 2012-02-01 13:48:17

5

只需使用ConcurrentDictionary<TKey, TValue>

注意ConcurrentDictionary<TKey, TValue>.GetEnumerator是線程安全的:

普查員從字典返回的安全與讀取和寫入字典

+0

這是*答案,假設OP有.NET4可供使用。 – LukeH 2012-02-01 13:54:44

+1

這是因爲它可能會創建一個集合的副本? OP已經知道這是一種自動防故障解決方案。 – Groo 2012-02-01 13:58:33

+0

我實際上並不限於.net3.5,我查看了'ConcurrentDictionary.Keys'的源代碼,並且它爲每個調用返回一個新的列表(顯然安全),所以我想我會在該鎖下執行相同的操作。有什麼不好的是,它是較慢,但這可能是線程安全 – wal 2012-02-01 13:59:28