[英]linq deferred execution when using locks in methods that return IEnumerable
Consider a simple Registry
class accessed by multiple threads: 考虑一个由多个线程访问的简单Registry
类:
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;
}
}
}
}
and typical usage: 和典型用法:
var ids1 = _registry.Ids;//execution deferred until line below
var ids2 = ids1.Select(p => p).ToArray();
This class is not thread safe as it's possible to receive System.InvalidOperationException
此类不是线程安全的,因为它可以接收System.InvalidOperationException
Collection was modified; 收集被修改; enumeration operation may not execute. 枚举操作可能无法执行。
when ids2 is assigned if another thread calls Register
as the execution of _items.Keys
is not performed under the lock! 当ids2被分配时, 如果另一个线程调用Register
作为执行_items.Keys
不在锁定下执行!
This can be rectified by modifying Ids
to return an IList
: 这可以通过修改Ids
以返回IList
来纠正:
public IList<int> Ids
{
get
{
lock (_lock)
{
return _items.Keys.ToList();
}
}
}
but then you lose a lot of the 'goodness' of deferred execution, for example 但是,例如,你失去了许多延迟执行的“善”
var ids = _registry.Ids.First(); //much slower!
So, 所以,
1) In this particular case are there any thread-safe options that involve IEnumerable
1)在这种特殊情况下,有任何涉及IEnumerable
线程安全选项
2) What are some best practices when working with IEnumerable
and locks ? 2)使用IEnumerable
和锁时有哪些最佳实践?
When your Ids
property is accessed then the dictionary cannot be updated, however there is nothing to stop the Dictionary from being updated at the same time as LINQ deferred execution of the IEnumerator<int>
it got from Ids
. 当你的Ids
属性被访问时,字典无法更新,但是没有什么可以阻止字典在LINQ延迟执行它从Ids
获得的IEnumerator<int>
的同时更新。
Calling .ToArray()
or .ToList()
inside the Ids
property and inside a lock will eliminate the threading issue here so long as the update of the dictionary is also locked. 只要字典的更新也被锁定,在Ids
属性内部和锁内部调用.ToArray()
或.ToList()
将消除线程问题。 Without locking both update of the dictionary and ToArray()
, it is still possible to cause a race condition as internally .ToArray()
and .ToList()
operate on IEnumerable. 在不锁定字典和ToArray()
更新的情况下,仍然可以在内部引起竞争条件.ToArray()
和.ToList()
在IEnumerable上运行。
In order to resolve this you need to either take the performance hit of ToArray
inside a lock, plus lock your dictionary update, or you can create a custom IEnumerator<int>
that itself is thread safe. 为了解决这个问题,您需要在锁内部获取ToArray
的性能,并锁定字典更新,或者您可以创建自己的线程安全的自定义IEnumerator<int>
。 Only through control of iteration (and locking at that point), or through locking around an array copy can you achieve this. 只有通过控制迭代(并在该点锁定),或通过锁定数组副本才能实现这一目标。
Some examples can be found below: 下面是一些例子:
Just use ConcurrentDictionary<TKey, TValue>
. 只需使用ConcurrentDictionary<TKey, TValue>
。
Note that ConcurrentDictionary<TKey, TValue>.GetEnumerator
is thread-safe: 请注意, ConcurrentDictionary<TKey, TValue>.GetEnumerator
是线程安全的:
The enumerator returned from the dictionary is safe to use concurrently with reads and writes to the dictionary 从字典返回的枚举器可以安全地与字典的读写一起使用
If you use yield return
inside the property, then compiler will ensure that the lock is taken on first call to MoveNext()
, and released when the enumerator is disposed. 如果在属性中使用yield return
,则编译器将确保在第一次调用MoveNext()
时MoveNext()
锁定,并在释放枚举数时释放。
I would avoid using this, however, because poorly implemented caller code might forget to call Dispose
, creating a deadlock. 但是,我会避免使用它,因为实现不好的调用者代码可能会忘记调用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;
}
}
}
声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.