Подтвердить что ты не робот

Linq отложенное выполнение при использовании блокировок в методах, возвращающих IEnumerable

Рассмотрим простой класс 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;
            }
        }
    }
}

и типичное использование:

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

Этот класс не является потокобезопасным, так как можно получить System.InvalidOperationException

Коллекция была изменена; операция перечисления не может выполняться.

когда ids2 присваивается , если другой поток вызывает Register, поскольку выполнение _items.Keys не выполняется под блокировкой!

Это можно устранить, изменив Ids, чтобы вернуть IList:

public IList<int> Ids
    {
        get
        {
            lock (_lock)
            {
                return _items.Keys.ToList();
            }
        }
    }

но затем вы теряете много "доброты" отсроченного исполнения, например

var ids = _registry.Ids.First();  //much slower!

Итак,
1) В этом конкретном случае есть ли какие-либо поточно-безопасные варианты, которые включают IEnumerable
2) Каковы некоторые рекомендации при работе с IEnumerable и блокировками?

4b9b3361

Ответ 1

Когда ваше свойство Ids получает доступ, словарь не может быть обновлен, однако нет ничего, что могло бы остановить обновление Словаря одновременно с LINQ отложенным исполнением IEnumerator<int>, полученным от Ids.

Вызов .ToArray() или .ToList() внутри свойства Ids и внутри блокировки устранит проблему с потоками, если обновление словаря также заблокировано. Без блокировки как обновления словаря, так и ToArray(), по-прежнему можно вызвать условие гонки, так как внутри .ToArray() и .ToList() работают с IEnumerable.

Чтобы решить эту проблему, вам нужно либо выполнить удар производительности ToArray внутри блокировки, либо заблокировать обновление словаря, либо создать собственный IEnumerator<int>, который сам по себе является потокобезопасным. Только путем управления итерацией (и блокировкой в ​​этой точке) или путем блокировки вокруг копии массива вы можете достичь этого.

Некоторые примеры можно найти ниже:

Ответ 2

Просто используйте ConcurrentDictionary<TKey, TValue>.

Обратите внимание, что ConcurrentDictionary<TKey, TValue>.GetEnumerator является потокобезопасным:

Перечислитель, возвращаемый из словаря, безопасен для одновременного использования при чтении и записи в словаре

Ответ 3

Если вы используете 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;
        }         
    }     
}