【问题标题】:Partially thread-safe dictionary部分线程安全的字典
【发布时间】:2011-01-17 11:53:14
【问题描述】:

我有一个类维护一个私有的Dictionary 实例,该实例缓存一些数据。

该类使用ReaderWriterLockSlim 从多个线程写入字典。

我想在类之外公开字典的值。
什么是线程安全的方法?

现在,我有以下内容:

public ReadOnlyCollection<MyClass> Values() {
    using (sync.ReadLock())
        return new ReadOnlyCollection<MyClass>(cache.Values.ToArray()); 
}

有没有办法在不多次复制集合的情况下做到这一点?

我使用的是 .Net 3.5(不是 4.0)

【问题讨论】:

    标签: c# .net thread-safety


    【解决方案1】:

    我想在类之外公开字典的值。 什么是线程安全的方法?

    你有三个选择。

    1) 制作数据副本,分发副本。优点:不用担心线程安全地访问数据。缺点:客户得到一份过期数据的副本,而不是最新的数据。此外,复制很昂贵。

    2) 分发一个对象,该对象在读取基础集合时会锁定它。您必须编写自己的只读集合,该集合引用“父”集合的锁。仔细设计这两个对象,使死锁是不可能的。优点:从客户的角度来看“有效”;他们无需担心锁定即可获得最新数据。缺点:为您提供更多工作。

    3) 将问题推给客户。公开锁,并要求客户端在使用数据之前自己锁定数据的所有视图。优点:对你没有工作。缺点:为客户做更多的工作,他们可能不愿意或不能做的工作。死锁等风险现在成为客户的问题,而不是您的问题。

    【讨论】:

      【解决方案2】:

      如果您想要字典当前状态的快照,那么对于这种集合类型确实无能为力。这与 ConcurrentDictionary&lt;TKey, TValue&gt;.Values 属性使用的技术相同。

      如果您不介意在枚举时修改集合而抛出InvalidOperationException,您可以只返回cache.Values,因为它是只读的(因此不会损坏字典数据)。

      【讨论】:

        【解决方案3】:

        编辑:我个人认为下面的代码在技术上正确回答了您的问题(例如,它提供了一种在不创建副本的情况下枚举集合中的值的方法)。一些开发人员的声望远比我强烈建议反对这种方法,因为他们在编辑/cmets 中解释了原因。简而言之:这显然是个坏主意。因此我留下了答案,但建议你不要使用它。


        除非我遗漏了什么,否则我相信您可以将您的值公开为IEnumerable&lt;MyClass&gt;,而无需使用yield 关键字复制值:

        public IEnumerable<MyClass> Values {
            get {
                using (sync.ReadLock()) {
                    foreach (MyClass value in cache.Values)
                        yield return value;
                }
            }
        }
        

        但是请注意(我猜你已经知道了),这种方法提供了惰性评估,这意味着上面实现的 Values 属性可以不 被视为提供快照

        换句话说……好吧,看看这段代码(我当然猜测你这个类的一些细节):

        var d = new ThreadSafeDictionary<string, string>();
        
        // d is empty right now
        IEnumerable<string> values = d.Values;
        
        d.Add("someKey", "someValue");
        
        // if values were a snapshot, this would output nothing...
        // but in FACT, since it is lazily evaluated, it will now have
        // what is CURRENTLY in d.Values ("someValue")
        foreach (string s in values) {
            Console.WriteLine(s);
        }
        

        因此,如果要求此 Values 属性等同于访问该属性时 cache 中的内容的快照,那么您将不得不一份。

        (从 280Z28 开始):以下是不熟悉“C# 做事方式”的人如何锁定代码的示例:

        IEnumerator enumerator = obj.Values.GetEnumerator();
        MyClass first = null;
        if (enumerator.MoveNext())
            first = enumerator.Current;
        

        (结束 280Z28)

        【讨论】:

        • 你的yield break 没用。此外,这可以是属性而不是方法。 (因为它不做昂贵的副本)
        • @Dan:我会强烈建议避免这种情况。潜在的性能改进带来了在只读模式下死锁字典的巨大风险。对于线程安全代码,线程安全总是胜过性能。
        • @Dan:我添加了一个会锁定字典的“写得不好但无辜的代码”的示例。
        • 这是一个非常非常糟糕的主意。 在 yield 和下一次迭代之间可以经过任意多的时间,在此期间其他代码可能会触发导致其他线程的事件尝试访问该集合。这段代码给用户一个定时炸弹,当它发生时会导致死锁。 当您无法控制 (1) 在锁定中花费多少时间,以及 (2) 在锁定期间可以运行哪些代码时,请始终避免使用锁定
        • @280Z28:这是一个非常有用的观察结果,老实说,我绝对没有考虑过。就个人而言,虽然我普遍同意“线程安全......胜过性能”,但我也认为在死锁可能的场景之间存在区别——但可以避免,如果开发团队得到了适当的通知——考虑到特定类/方法/等的设计,死锁是不可避免的。在性能关键的场景中,如果上述方法提供了性能改进,我可能仍会选择它(我什至不知道;我没有对其进行分析)。
        【解决方案4】:

        查看下一个可能性,只公开 ICollection 接口,因此在 Values() 中您可以返回您自己的实现。此实现将仅使用 Dictioanry.Values 上的引用,并始终使用 ReadLock 访问项目。

        【讨论】:

        • 这不起作用,因为锁是私有的。 (类的消费者不能进入锁)
        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2011-10-20
        • 2010-10-24
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多