简体   繁体   English

在返回IEnumerable的方法中使用锁时,linq延迟执行

[英]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.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM