Parallel.ForEach 与 ReaderWriterLockSlim 中的死锁

发布于 2025-01-06 09:07:27 字数 2734 浏览 6 评论 0原文

我的应用程序中存在一个有趣的死锁问题。有一个内存数据存储使用 ReaderWriterLockSlim 来同步读取和写入。其中一个读取方法使用 Parallel.ForEach 在给定一组过滤器的情况下搜索存储。其中一个过滤器可能需要恒定时间读取同一存储。以下是产生死锁的场景:

更新:下面的示例代码。使用实际方法调用更新了步骤
给定 ConcreteStoreThatExtendsGenericStore 的单例实例 store

  1. Thread1 获取存储上的读锁 - store.Search(someCriteria)
  2. < strong>Thread2 尝试使用写锁更新存储 - store.Update() -,在 Thread1
  3. Thread1 执行 之后阻塞并行.ForEach针对存储运行一组过滤器
  4. Thread3(由 Thread1 的 Parallel.ForEach 生成)尝试对存储进行常量读取。它尝试获取读锁,但被 Thread2 的写锁阻止。
  5. Thread1 无法完成,因为它无法加入Thread3Thread2 无法完成,因为它被阻塞在 Thread1 后面。

理想情况下,如果当前线程的祖先线程已经拥有相同的锁,我想要做的是尝试获取读锁。有什么办法可以做到这一点吗?或者还有另一种/更好的方法吗?

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;
    }
}

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;
    }
}

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(1

倾`听者〃 2025-01-13 09:07:27

目前还不清楚您实际需要什么样的并发性、内存和性能要求,因此这里有一些选择。

如果您使用的是 .Net 4.0,则可以将 Dictionary 替换为 ConcurrentDictionary 并删除 ReaderWriterLockSlim。请记住,这样做将减少锁定范围并更改方法语义,允许在枚举时更改内容(除其他外),但另一方面,这将为您提供一个不会阻塞的线程安全枚举器读取或写入。您必须确定这对于您的情况是否是可接受的改变。

如果您确实需要以这种方式锁定整个集合,并且可以将所有操作保持在同一状态,那么您也许可以支持递归锁定策略 (new ReaderWriterLockSlim(LockRecursionPolicy.SupportsRecursion))线。并行执行搜索是否有必要?

或者,您可能只想获取当前值集合的快照(锁定该操作),然后针对该快照执行搜索。它不能保证拥有最新数据,并且您必须花费一些时间进行转换,但对于您的情况来说,这可能是一个可以接受的权衡。

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.

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文