【问题标题】:Any downsides to locking a collection vs. a syncRoot?锁定集合与同步根有什么缺点吗?
【发布时间】:2010-10-11 23:22:22
【问题描述】:

我想知道锁定 List<T>HashSet<T>Dictionary<TKey, TValue> 等集合而不是简单的 object 是否有任何缺点。

注意:在以下示例中,这是唯一发生锁的地方,它不是从多个位置锁定的,而是可以从多个线程调用静态方法。此外,_dict 永远不会在 GetSomething 方法之外访问。

我当前的代码如下所示:

private static readonly Dictionary<string, string> _dict = new Dictionary<string, string>();
public static string GetSomething(string key)
{
    string result;
    if (!_dict.TryGetValue(key, out result))
    {
        lock (_dict)
        {
            if (!_dict.TryGetValue(key, out result))
            {
                _dict[key] = result = CalculateSomethingExpensive(key);
            }
        }
    }
    return result;
}

另一位开发人员告诉我锁定集合会导致问题,但我对此表示怀疑。如果我这样做,我的代码会更有效率吗?

private static readonly Dictionary<string, string> _dict = new Dictionary<string, string>();
private static readonly object _syncRoot = new object();
public static string GetSomething(string key)
{
    string result;
    if (!_dict.TryGetValue(key, out result))
    {
        lock (_syncRoot)
        {
            if (!_dict.TryGetValue(key, out result))
            {
                _dict[key] = result = CalculateSomethingExpensive(key);
            }
        }
    }
    return result;
}

【问题讨论】:

  • 您是否考虑将 _dict 和 GetSomething() 移到单独的类中?它显然做了一些与班级其他人不同且无关的事情。 (它看起来像一个记忆模式)
  • 您必须将 _dict 声明为 volatile 才能有机会正确处理。另请参阅有关双重检查锁定及其陷阱的答案:stackoverflow.com/questions/394898/…
  • @Sjoerd volatile 在许多情况下对 C# 进行双重检查是不必要的。但是,这里的双重检查不起作用,因为它不是双重检查字段读取而是双重检查方法调用,它仅在调用的方法是原子的情况下才有效,这不是,所以双重检查这里完全错了。
  • @Jon Hanna True。这显示了双重检查锁定有多少陷阱。
  • @Sjoerd,是的。我通常认为它是一种优化而不是“正常”技术,但是有时可以通过做通常被认为是优化的事情来减轻死锁的风险!尽管如此,仔细检查总是可疑的,在这种情况下非常危险,以至于我的答案花在这上面的时间比其他任何事情都多。

标签: c# .net multithreading locking


【解决方案1】:

如果您将您的收藏暴露给外界,那么,是的,这可能是个问题。通常的建议是锁定您专有的东西,并且永远不会被您影响之外的代码意外锁定。这就是为什么通常最好锁定您甚至从未考虑公开的东西(即为此目的创建的特定锁定对象)。这样,当您的记忆力下降时,您将永远可能不会得到意想不到的结果。

更直接地回答您的问题:将另一个对象添加到组合中永远不会更有效率,但是将通常被认为是良好的编码实践放在一些感知但未衡量的效率之前可能是过早发生的优化。我赞成最佳做法,直到它明显造成瓶颈。

【讨论】:

  • 其实这样的情况,死锁风险会增加很多!!
  • 正如我所说,集合永远不会暴露,只能通过单一方法访问。
  • 只要您确定是这种情况,就没有问题。因为我又笨又健忘,所以我总是喜欢使用专门用于锁定的对象。在充满危险的多线程世界中,这是一件少担心的事情。
  • +1 表示重视良好的代码实践而不是无法衡量的效率。
  • “用于锁定的对象”并不总是足够好,并且可能导致死锁。需要的是每个相关锁定要求的对象,因此如果一个类有两组需要同步但彼此不同步的操作,它应该有两个这样的对象,依此类推。一个类可能需要许多这样的对象。当您看到很多这样的对象时,尽管它通常是对非常细粒度的锁定而不是确保正确性的优化(并且开始以不同的方式使正确性变得更加困难)。这可能会导致更复杂的死锁。没有快速的规则。
【解决方案2】:

在这种情况下,我会锁定集合;锁的用途与集合直接相关,与任何其他对象无关,因此将其用作锁对象有一定程度的自我注释。

不过我会做出一些改变。

我在文档中找不到任何内容来说明 TryGetValue 是线程安全的,并且如果您在字典处于无效状态时调用它不会抛出异常(或更糟),因为它是在添加的中途一个新的价值。因为它不是原子的,所以您在此处使用的双读模式(以避免花费时间获得锁)是不安全的。必须将其更改为:

private static readonly Dictionary<string, string> _dict = new Dictionary<string, string>();
public static string GetSomething(string key)
{
    string result;
    lock (_dict)
    {
        if (!_dict.TryGetValue(key, out result))
        {
            _dict[key] = result = CalculateSomethingExpensive(key);
        }
    }
    return result;
}

如果成功的读取可能比不成功的读取更多(因此需要写入),则使用 ReaderWriterLockSlim 会在这些读取上提供更好的并发性。

编辑:我刚刚注意到您的问题一般不是关于偏好,而是关于效率。确实,在整个系统中多使用 4 字节内存(因为它是静态的)的效率差异绝对为零。该决定根本与效率无关,但由于两者具有相同的技术优势(在这种情况下),是关于您是否发现锁定在集合或单独的对象上更能表达您的意图给其他开发者(包括将来的你)。

【讨论】:

    【解决方案3】:

    没有。只要不能从其他任何地方访问该变量,并且您可以保证仅在此处使用锁,就没有缺点。事实上,Monitor.Enter 的文档(这是 C# 中的 lock 使用的)正是这样做的。

    但是,作为一般规则,我仍然建议使用私有对象进行锁定。这通常更安全,并且如果您将此对象暴露给任何其他代码,它将保护您,因为您不会打开您的对象被其他代码锁定的可能性。

    【讨论】:

      【解决方案4】:

      直接回答您的问题:

      无论你锁定什么对象都没有区别。 .NET 只关心它的引用,它的工作方式与指针完全一样。将 .NET 中的锁定视为一个大的同步哈希表,其中键是对象引用,值是布尔值,表示您可以进入或不进入监视器。如果两个线程锁定到不同的对象 (a != b),它们可以同时进入锁的监视器,即使 a.Equals(b)(这非常重要!!!)。但如果他们锁定 a 和 b,并且 (a==b) 一次只有其中一个会出现在监视器中。

      只要没有在您的范围之外访问 dict,您就不会影响性能。如果 dict 在其他地方可见,其他用户代码可能会锁定它,即使不是必需的(认为你的同桌很笨,会锁定他在代码中找到的第一个随机对象)。

      希望能有所帮助。

      【讨论】:

        【解决方案5】:

        我建议使用 ICollection.SyncRoot 对象而不是您自己的对象进行锁定:

            private static readonly Dictionary<String, String> _dict = new Dictionary<String, String>();
            private static readonly Object _syncRoot = ((ICollection)_dict).SyncRoot;
        

        【讨论】:

        • SyncRoot 仅受支持,因为如果它们不破坏 ICollection 接口的定义,则必须如此。它被广泛描述为一个错误,这就是它没有包含在 ICollection 中的原因。使用它的现有代码应该重构为不再依赖它。新代码当然不应该使用它。
        • 谢谢乔恩,我没听说过
        • 看看这个关于 SyncRoot 模式的答案:stackoverflow.com/a/12425979/355438
        猜你喜欢
        • 2020-11-16
        • 2014-12-24
        • 1970-01-01
        • 2011-05-11
        • 1970-01-01
        • 2023-01-07
        • 2011-09-24
        • 2023-03-03
        相关资源
        最近更新 更多