Тупик в Parallel.ForEach с ReaderWriterLockSlim - PullRequest
5 голосов
/ 17 февраля 2012

У меня есть интересная проблема с тупиками в моем приложении.Существует хранилище данных в памяти, которое использует ReaderWriterLockSlim для синхронизации чтения и записи.Один из методов чтения использует Parallel.ForEach для поиска в хранилище по заданному набору фильтров.Вполне возможно, что один из фильтров требует постоянного чтения из одного и того же хранилища.Вот сценарий, который создает тупик:

ОБНОВЛЕНИЕ: Пример кода ниже.Шаги, обновленные фактическими вызовами методов
Для заданного синглтона store из ConcreteStoreThatExtendsGenericStore

  1. Thread1 получает блокировку чтения в хранилище - store.Search(someCriteria)
  2. Thread2 пытается обновить хранилище с блокировкой записи - store.Update() -, блоки за Thread1
  3. Thread1 выполняет Parallel.ForEachдля хранилища для запуска набора фильтров
  4. Thread3 (порождается Thread1 's Parallel.ForEach) пытается чтение хранилища в постоянном времени.Он пытается получить блокировку чтения, но заблокирован за Thread2 блокировкой записи.
  5. Thread1 не может завершить, потому что не может присоединиться Thread3 . Тема 2 не может закончить, потому что она заблокирована за Тема 1 .

В идеале я бы хотел не попытатьсяполучить блокировку чтения, если поток-предок текущего потока уже имеет такую ​​же блокировку.Есть какой-либо способ сделать это?Или есть другой / лучший подход?

public abstract class GenericStore<TKey, TValue>
{
    private ReaderWriterLockSlim _lock = new ReaderWriterLockSlim();
    private List<IFilter> _filters;  //contains instance of ExampleOffendingFilter

    protected Dictionary<TKey, TValue> Store { get; private set; }

    public void Update()
    {
        _lock.EnterWriterLock();
        //update the store
        _lock.ExitWriteLock();
    }

    public TValue GetByKey(TKey key)
    {
        TValue value;
        //TODO don't enter read lock if current thread 
        //was started by a thread holding this lock
        _lock.EnterReadLock();
        value = Store[key];
        _lock.ExitReadLock();
        return value;
    }

    public List<TValue> Search(Criteria criteria)
    {
        List<TValue> matches = new List<TValue>();
        //TODO don't enter read lock if current thread 
        //was started by a thread holding this lock
        _lock.EnterReadLock();
        Parallel.ForEach(Store.Values, item =>
        {
            bool isMatch = true;
            foreach(IFilter filter in _filters)
            {
                if (!filter.Check(criteria, item))
                {
                    isMatch = false;
                    break;
                }
            }
            if (isMatch)
            {
                lock(matches)
                {
                    matches.Add(item);
                }
            }
        });
        _lock.ExitReadLock();
        return matches;
    }
}

public class ExampleOffendingFilter : IFilter
{
    private ConcreteStoreThatExtendsGenericStore _sameStore;

    public bool Check(Criteria criteria, ConcreteValueType item)
    {
        _sameStore.GetByKey(item.SomeRelatedProperty);
        return trueOrFalse;
    }
}

1 Ответ

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

Неясно, какие требования к параллелизму, памяти и производительности у вас действительно есть, поэтому вот несколько вариантов.

Если вы используете .Net 4.0, вы можете заменить Dictionary на ConcurrentDictionaryи удалите свой ReaderWriterLockSlim.Имейте в виду, что это уменьшит объем блокировки и изменит семантику методов, позволяя вносить изменения в содержимое во время перечисления (среди прочего), но с другой стороны, это даст вам поточный безопасный перечислитель, который не будет блокироватьчитает или пишет.Вам нужно будет определить, является ли это приемлемым изменением для вашей ситуации.

Если вам действительно нужно заблокировать всю коллекцию таким образом, возможно, вы сможете поддержать политику рекурсивной блокировки (new ReaderWriterLockSlim(LockRecursionPolicy.SupportsRecursion)) если вы можете сохранить все операции в одном потоке.Является ли выполнение поиска параллельно необходимостью?

В качестве альтернативы, вы можете просто получить снимок текущего набора значений (блокируя эту операцию), а затем выполнить поиск по снимку.Не гарантируется наличие последних данных, и вам придется потратить немного времени на конверсию, но, возможно, это приемлемый компромисс для вашей ситуации.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...