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

wal*_*wal 13 c# linq thread-safety

考虑一个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;
            }
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

和典型用法:

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

这个类不是线程安全的,因为它可以接收 System.InvalidOperationException

收集被修改; 枚举操作可能无法执行.

如果另一个线程调用Register,则分配ids2 _items.Keys,因为锁定下没有执行!

这可以通过修改Ids返回IList:

public IList<int> Ids
    {
        get
        {
            lock (_lock)
            {
                return _items.Keys.ToList();
            }
        }
    }
Run Code Online (Sandbox Code Playgroud)

但是,例如,你失去了许多延迟执行的"善"

var ids = _registry.Ids.First();  //much slower!
Run Code Online (Sandbox Code Playgroud)

因此,
1)在这种特殊情况下,是否存在涉及的任何线程安全选项IEnumerable
2)使用IEnumerable和锁定时有哪些最佳实践?

Dr.*_*ABT 8

Ids访问您的属性时,无法更新字典,但是没有什么可以阻止字典在LINQ延迟执行IEnumerator<int>它的同时进行更新Ids.

只要字典的更新也被锁定,调用.ToArray().ToList()Ids属性内部和锁内将消除线程问题.在不锁定字典更新的情况下ToArray(),仍然可以在内部引起竞争条件.ToArray().ToList()在IEnumerable上操作.

为了解决这个问题,您需要获取ToArray锁定内部的性能,加上锁定字典更新,或者您可以创建一个IEnumerator<int>本身是线程安全的自定义.只有通过控制迭代(并在该点锁定),或通过锁定数组副本才能实现这一目标.

下面是一些例子:


jas*_*son 5

只是用ConcurrentDictionary<TKey, TValue>.

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

从字典返回的枚举器可以安全地与字典的读写一起使用