5

I have an interesting problem with deadlocks in an my application. There is an in-memory data store that uses a ReaderWriterLockSlim to synchronize reads and writes. One of the read methods uses Parallel.ForEach to search the store given a set of filters. It's possible that one of the filters requires a constant-time read of same store. Here is the scenario that's producing a a deadlock:

UPDATE: Example code below. Steps updated with actual method calls
Given singleton instance store of ConcreteStoreThatExtendsGenericStore

  1. Thread1 gets a read lock on the store - store.Search(someCriteria)
  2. Thread2 attempts to update the store with a write lock - store.Update() -, blocks behind Thread1
  3. Thread1 executes Parallel.ForEach against the store to run a set of filters
  4. Thread3 (spawned by Thread1's Parallel.ForEach) attempts a constant-time read of the store. It tries to get a read lock but is blocked behind Thread2's write lock.
  5. Thread1 cannot finish because it can't join Thread3. Thread2 can't finish because it's blocked behind Thread1.

Ideally what I'd like to do is not try to acquire a read lock if an ancestor thread of the current thread already has the same lock. Is there any way to do this? Or is there a another/better approach?

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;
    }
}
eakins05
  • 78
  • 6
  • Is the in-memory store a custom type or is it a List which is a field inside another class? – Trevor Pilley Feb 16 '12 at 23:16
  • Passs what you have locked into the method you are running in foreach, otherwise parallelising it from read onwards is a waste of time. Never going to run in parallel because they are all bottlenecked on the same resource. – Tony Hopkinson Feb 16 '12 at 23:30
  • @TrevorPilley: The store is a Dictionary, with Parallel.ForEach over the Values – eakins05 Feb 17 '12 at 14:36
  • @TonyHopkinson: That works perfect for the simple scenario. The more complex scenario is when one store has to read from another store, where it reads from a different one, and ends up back to the original one (circular dependency). We can guard this through design and code reviews, but I'd still prefer to be protected with a generic fail safe solution. – eakins05 Feb 17 '12 at 14:42
  • Irrelevant, if you want to lock for read, then only one task braketed by the lock can execute, so what use is parallel? Either they can rely on the owning thread's lock, or they run in series, whatever you do. You might get somewhere with reducing the lifetime of the lock, but that is hugely dependant on what you are doing while it is, and constantly locking unlocking, and blocking might make parallelisation slower. If you want to use TPL, you need to reorganise your code, so less locking is needed. – Tony Hopkinson Feb 18 '12 at 22:43
  • The ReaderWriterLockSlim allows concurrent reads, does it not? The entering of the read lock is to block against/behind writers, not other readers. – eakins05 Feb 20 '12 at 14:33

1 Answers1

2

It's unclear what kind of concurrency, memory and performance requirements you actually have so here are a few options.

If you are using .Net 4.0, you could replace your Dictionary with a ConcurrentDictionary and remove your ReaderWriterLockSlim. Keep in mind that doing that will reduce your locking scope and change your method semantics, allowing changes to the contents while you're enumerating (among other things), but on the other hand that will give you a threadsafe enumerator that won't block reads or writes. You'll have to determine if that's an acceptable change for your situation.

If you really do need to lock down the entire collection in this way, you might be able to support a recursive lock policy (new ReaderWriterLockSlim(LockRecursionPolicy.SupportsRecursion)) if you can keep all operations on the same thread. Is performing your search in parallel a necessity?

Alternately, you may want to just get a snapshot of your current collection of values (locking around that operation) and then perform your search against the snapshot. It won't be guaranteed to have the latest data and you'll have to spend a little time on conversion, but maybe that's an acceptable tradeoff for your situation.

Chris Hannon
  • 4,134
  • 1
  • 21
  • 26
  • I like the ConcurrentDictionary approach, but unfortunately we maintain more than just the Storage (indexes, etc), which have to be behind the write lock. Removing parallelization may be an option that we'll have to POC out. The snapshot idea is something we've toyed with before, but with a paradigm shift like that, we're contemplating purchasing a caching solution to replace this legacy homegrown one, so we'd probably go that route anyways. Thanks for the insight! – eakins05 Feb 20 '12 at 14:40