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
和锁?
当访问您的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;
}
}
}
我不相信它的确如此。我刚刚测试了这一点,它也失败了。在上面的例子中,只有在分配'ids2'时才会命中收益率回报,即在锁外 – wal 2012-02-01 13:32:21
@Groo no时,只要退出方法,它就会释放锁。该方法本身只是返回一个新的枚举。 – Polity 2012-02-01 13:47:41
@Groo:编译器*不会释放每个yield之间的锁。它只是在调用枚举器对象的第一个'MoveNext'调用之前不需要锁定。这本身导致了这个代码的另一个问题:一个恶意和/或愚蠢的调用者可能会做'Ids.GetEnumerator()。MoveNext()',然后该锁被永久保存;该对象变得不可用。 – LukeH 2012-02-01 13:48:17
只需使用ConcurrentDictionary<TKey, TValue>
。
注意ConcurrentDictionary<TKey, TValue>.GetEnumerator
是线程安全的:
普查员从字典返回的安全与读取和写入字典
问题同时使用不返回值的属性ID,它指的是Dictionary.Keys,它是一个共享实例(对同一字典实例的每次调用都返回相同的密钥集合实例)。或者像第二个实现那样创建一个副本,或者使用ConcurrentDictionary类 – Polity 2012-02-01 13:15:12
只需使用'ConcurrentDictionary',然后转储锁定:http://msdn.microsoft.com/en-us/library/dd287191.aspx –
LukeH
2012-02-01 13:15:35
请注意,您在你的第一个片段中重新查看字典的关键集合,这不是一个流式序列。这与你的List版本在这方面没有多大区别。 – 2012-02-01 13:17:56