LINQ延迟执行中返回IEnumerable的

LINQ延迟执行中返回IEnumerable的

问题描述:

考虑一个简单的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

当访问您的Ids属性,则该词典不能被更新,但没有什么可以从在同一时间为IEnumerator<int>Ids得到了LINQ延迟执行更新停止字典。

Ids属性内调用.ToArray().ToList(),并且锁内部将消除此处的线程问题,只要字典的更新也被锁定。不锁定字典和ToArray()的更新,它仍然可能导致竞争条件,因为内部.ToArray().ToList()对IEnumerable进行操作。

为了解决这个问题,你需要在锁内取得ToArray的性能命中率,并锁定你的字典更新,或者你可以创建一个自己的线程安全的自定义IEnumerator<int>。只有通过控制迭代(并在该点锁定),或者通过锁定数组副本,才能实现这一点。

如果使用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

只需使用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