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
и блокировками?
Ответы
Ответ 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;
}
}
}