【问题标题】:Is this the correct object to lock on for thread safety?这是为了线程安全而锁定的正确对象吗?
【发布时间】:2026-02-10 00:35:01
【问题描述】:

下面的代码将从网站调用,因此非静态类中的静态字典对象需要是线程安全的。基本上,代码的目的是封装逻辑并维护存储在 CounterContainer 实例中的 perfmon 计数器的生命周期。构造函数被称为传入instanceName。构造函数需要检查该 instanceName 的 CounterContainer 是否已定义并存储在字典中。如果是这样,它可以(并且必须)使用该实例。如果不是,它会创建 CounterContainer 的实例,将其存储在字典中,然后使用该实例。要使用的 CounterContainer 的实例存储在一个非静态成员中,因此从那时起就是线程安全的。

作为使用静态字典的唯一位置是在构造函数中,我觉得在访问字典期间锁定字典是安全的吗?这是否会在以后导致任何不可预见的问题,例如阻塞/死锁?我什么都看不到,但过去真的没有必要过多地考虑这种事情。

我也考虑过 lock(this): 但我认为这不会起作用,因为它只会锁定正在创建的 PerformanceCounters 实例,而不是底层静态字典(所以不会是线程安全的)。

namespace ToolKit
{
    using System;
    using System.Diagnostics;
    using System.Collections.Generic;

    public class PerformanceCounters : IPerformanceCounters
    {
        private static Dictionary<string, CounterContainer> _containers = new Dictionary<string, CounterContainer>();
        private CounterContainer _instanceContainer;

        public PerformanceCounters(string instanceName)
        {
            if (instanceName == null) throw new ArgumentNullException("instanceName");
            if (string.IsNullOrWhiteSpace(instanceName)) throw new ArgumentException("instanceName");

            // Is this the best item to lock on?
            lock (_containers)
            {

                if (_containers.ContainsKey(instanceName))
                {
                    _instanceContainer = _containers[instanceName];
                    return;
                }

                _instanceContainer = new CounterContainer(instanceName);
                _containers.Add(instanceName, _instanceContainer);
            }
        }
        public void Start()
        {
            _instanceContainer.AvgSearchDuration.Start();
        }

        public void FinishAndLog()
        {
            _instanceContainer.SearchesExecuted.Increment();
            _instanceContainer.SearchesPerSecond.Increment();
            _instanceContainer.AvgSearchDuration.Increment();
        }
    }
}

【问题讨论】:

  • 你考虑过使用 ConcurrentDictionary 类吗?那么您就不必担心了 - 也可能与这个问题重复:*.com/questions/440957/…
  • @BrokenGlass:对不起,我错过了,是的,它们的情况几乎相同。直到 ConcurrentDictionary 类也被提出作为对此的回应时才听说过。

标签: c# multithreading thread-safety


【解决方案1】:

不反对使用 ConcurrentDictionary 的建议,而是更笼统地回答:

  1. 最好的锁定解决方案是完全不锁定。有时在没有锁定的情况下使某些东西并发并不是很困难。下一个最好的方法是拥有细粒度的锁。然而,粗粒度锁更容易推理,因此对其正确性充满信心。除非你手头有一个经过良好测试的、具有适当目的的无锁类,否则从粗粒度锁(用同一个锁阻塞一大堆操作的锁)开始,然后再转向更细粒度的锁,因为有逻辑死锁(您可以看到两个不相关的操作可能会相互阻塞)或作为优化(如果需要)。
  2. 永远不要锁定this,因为您的代码外部的东西可能会锁定对象,并且您很容易陷入死锁或至少锁定争用只能通过查看类内部代码才能找到,并且调用代码(编写一个的人可能无法访问另一个)。出于类似原因,切勿锁定Type
  3. 出于类似原因,只锁定私人成员。这样,只有类内部的代码才能锁定它,并且锁定争用的可能性仅限于该位置。
  4. 避免在此类代码中使用partial,如果确实使用partial,请尝试将对象上的所有锁保持在同一个位置。这里没有技术原因,但当您需要考虑所有可能被锁定的位置时,它确实会有所帮助。
  5. 永远不要锁定值类型(整数类型、结构等)。这样做会将值装箱到一个新对象并锁定它。然后,当其他一些代码试图获得锁时,它会装箱到另一个对象并锁定它。本质上根本没有锁定(除非理论上优化,使用享元模式的拳击 [它没有] 但实际上会使事情变得更糟)。
  6. 有时锁的用途与使用锁时使用的单个对象有关,而在不使用锁时不使用该对象。在这种情况下,使用该对象作为锁定对象有助于代码的可读性,将锁与该对象关联。
  7. 拥有一个纯粹为了锁定目的而存在的对象永远不会错,所以如果您不确定,那就这样做吧。
  8. 如果任何受影响的对象是静态的,则锁定对象必须是静态的,否则可能是实例。相反,由于实例锁比静态锁更细粒度,因此实例在适用时会更好。

【讨论】:

  • 感谢您的详细回答,忘记了锁定值类型时会发生的装箱问题。
【解决方案2】:

是的,因为 _containers 是静态的。

你可能也想看看ReaderWriterLockSlim,这样你就想这么多(提高性能)

【讨论】:

  • +1 提醒我有关 ReaderWriterLocks 并且有一个苗条版本。我完全忘记了那些,自从我不得不担心线程以来已经有几年了。将@flq 标记为已接受的答案,因为我将使用他引用的 ConcurrentDictionary。
  • 没关系 - 我之前不知道 ConcurrentDictionary ;-)。你每天都在学习……
  • 高度怀疑在这种情况下 ReaderWriterLockSlim 实际上会比普通的旧 lock 慢。
  • @Brian Gideon - 为什么?每次使用 lock() 线程都会阻塞,即使项目在集合中也是如此。 ReaderWriterLock 只会在插入期间阻塞。
  • @Jakub:是的,但是ReaderWriterLockSlim 本身在服务锁开销方面比lock 慢 5 倍。由于 OP 持有锁的时间如此之短,因此可能不足以克服额外的开销。当读取器的数量显着超过写入器时,读取器-写入器锁的效果最好。顺便说一句,旧的ReaderWriterLock 的开销大约是普通的旧lock 的 15 倍。做一些基准测试,因为你的里程可能与我的不同。
【解决方案3】:

在我看来,有一个专门用于锁定的显式对象实例很常见。

private readonly object containerLock = new object();
...
lock (containerLock) {
 ...
}

另一个提示:如果您使用的是 .NET 4,请考虑使用 ConcurrentDictionary

【讨论】:

  • 但是 +1 用于引用 ConcurrentDictionary 作为 GetOrAdd(...) 传递函数以进行创建的能力看起来正是我想要做的。 @flq:谢谢,值得建立在@x0n 所说的基础上,以这种方式使用对象与“this”相同,因为两者都是基于实例的。如果您将对象设为静态,那么它与我提供的代码完全相同,但我锁定的是我正在使用的确切对象。
【解决方案4】:

是的,_containers 理论上是可以的,因为它的作用域与字典实例相同,也就是说,它是一个静态的(显然 - 它是字典实例。)

但是,一旦担心您的整体设计是您说它是托管在网站中的。每个 asp.net worker 都是一个单独的进程,因此当 IIS 由于空闲或其他原因回收工作进程时,您的静态字典可能会被破坏。此外,如果您使用网络花园(每个应用程序有多个工作人员),您的字典可能不止一个实例。

【讨论】:

  • 只要内存中至少有一个 .NET 引用实例处于活动状态,PERFMON 计数器就会保持活动状态。但是,即使内存中有两个以上的 .NET 引用,它们仍然会增加 PERFMON 计数器的单个实例。因此,同一台服务器上的多个 IIS 站点可以按我的意愿工作。一个网络农场也很好,因为我想在一个服务器一个服务器的基础上进行测量。但感谢您提出问题。