linq отложил выполнение при использовании блокировок в методах, которые возвращают IEnumerable - PullRequest
13 голосов
/ 01 февраля 2012

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

Ответы [ 3 ]

8 голосов
/ 01 февраля 2012

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

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

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

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

5 голосов
/ 01 февраля 2012

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

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

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

1 голос
/ 01 февраля 2012

Если вы используете 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;
        }         
    }     
} 
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...