【问题标题】:Why magic does an locking an instance of System.Object allow differently than locking a specific instance type?为什么锁定 System.Object 的实例与锁定特定实例类型不同?
【发布时间】:2012-01-11 14:19:20
【问题描述】:

我一直在学习线程锁定,但我没有找到解释为什么创建一个典型的 System.Object、锁定它并执行锁定期间所需的任何操作来提供线程安全?

示例

object obj = new object()
lock (obj) {
  //code here
}

起初我认为它只是在示例中用作占位符,并且打算与您正在处理的类型交换。但我发现 Dennis Phillips 指出的例子与实际使用 Object 实例没有什么不同。

因此,以需要更新私有字典为例,锁定 System.Object 的实例与实际锁定字典相比,如何提供线程安全(我知道在这种情况下锁定字典可能会出现同步问题) ? 如果字典是公开的呢?

//what if this was public?
private Dictionary<string, string> someDict = new Dictionary<string, string>();

var obj = new Object();
lock (obj) {
  //do something with the dictionary
}

【问题讨论】:

标签: c# multithreading locking


【解决方案1】:

lock 本身没有为Dictionary&lt;TKey, TValue&gt; 类型提供任何安全性。 lock 所做的基本上是

对于lock(objInstance) 的每次使用,对于给定对象(objInstance),lock 语句的主体中只会有一个线程

如果给定Dictionary&lt;TKey, TValue&gt; 实例的每次使用都发生在lock 内。并且每个lock 都使用相同的对象,然后您就知道一次只有一个线程在访问/修改字典。这对于防止多个线程同时读取和写入它并破坏其内部状态至关重要。

这种方法有一个大问题:您必须确保字典的每次使用都发生在锁内并且它使用相同的对象。如果您甚至忘记了一个,那么您已经创建了一个潜在的竞争条件,将不会有编译器警告,并且该错误可能会在一段时间内未被发现。

在第二个示例中,您展示了使用本地对象实例(var 表示本地方法)作为对象字段的锁定参数。这几乎肯定是错误的做法。本地将仅在方法的生命周期内存在。因此,对该方法的 2 次调用将在不同的局部变量上使用锁,因此所有方法将能够同时进入锁。

【讨论】:

    【解决方案2】:

    过去通常的做法是锁定共享数据本身:

    private Dictionary<string, string> someDict = new Dictionary<string, string>();
    
    lock (someDict ) 
    {
      //do something with the dictionary
    }
    

    但是(有点理论上的)反对意见是,您无法控制的其他代码也可能锁定someDict,然后您可能会出现死锁。

    因此建议使用一个(非常)私有的对象,声明为与数据一一对应,用作锁的替代品。只要访问字典的所有代码都锁定在obj 上,就可以保证胎面安全。

    // the following 2 lines belong together!!
    private Dictionary<string, string> someDict = new Dictionary<string, string>();
    private object obj = new Object();
    
    
    // multiple code segments like this
    lock (obj) 
    {
      //do something with the dictionary
    }
    

    所以obj 的目的是充当字典的代理,由于它的类型无关紧要,我们使用最简单的类型System.Object

    如果字典是公开的呢?

    那么所有的赌注都被取消了,任何代码都可以访问字典,并且包含类之外的代码甚至无法锁定保护对象。在你开始寻找修复之前,这根本不是一种可持续的模式。使用 ConcurrentDictionary 或将普通字典设为私有。

    【讨论】:

    • +1 提到其他代码可能使用您不知道的锁。例如字典在内部锁定自身。
    • 通过将对象实例放在实例成员级别,是否会锁定整个类?
    • 此外,如果两个线程试图调用一个方法,您似乎仍然可以在锁定 obj 的方法级别遇到死锁。如果我是正确的,有没有办法在方法级别管理请求?像请求队列一样?
    • @pghtech,a) 锁应与字典处于同一级别。 b)是的,您仍然可以遇到死锁,但它会是您的所有代码,因此您也可以修复它。
    【解决方案3】:

    用于锁定的对象与锁定期间修改的对象无关。它可以是任何东西,但应该是私有的并且没有字符串,因为公共对象可以在外部修改并且字符串可能被两个锁错误地使用。

    【讨论】:

      【解决方案4】:

      据我了解,使用通用object 只是为了锁定某些东西(作为内部可锁定对象)。为了更好地解释这一点;假设您在一个类中有两个方法,都访问Dictionary,但可能在不同的线程上运行。为了防止这两种方法同时修改Dictionary(并可能导致死锁),您可以锁定一些对象来控制流程。下面的例子可以更好地说明这一点:

      private readonly object mLock = new object();
      
      public void FirstMethod()
      {
          while (/* Running some operations */)
          {
              // Get the lock
              lock (mLock)
              {
                  // Add to the dictionary
                  mSomeDictionary.Add("Key", "Value");
              }
          }
      }
      
      public void SecondMethod()
      {
          while (/* Running some operation */)
          {
              // Get the lock
              lock (mLock)
              {
                  // Remove from dictionary
                  mSomeDictionary.Remove("Key");
              }
          }
      }
      

      在同一对象的两个方法中使用lock(...) 语句可防止这两个方法同时访问资源。

      【讨论】:

        【解决方案5】:

        你锁定的对象的重要规则是:

        1. 它必须是只对需要锁定它的代码可见的对象。这样可以避免其他代码也锁定它。

          1. 这排除了可能被保留的字符串和Type 对象。
          2. 这在大多数情况下排除了this,并且例外太少并且提供的利用很少,所以不要使用this
          3. 另请注意,框架内部的某些情况会锁定 Types 和 this,因此虽然“只要没有其他人这样做就可以”是正确的,但为时已晚。
        2. 它必须是静态的以保护静态静态操作,它可以是实例以保护实例操作(包括那些在静态中保存的实例内部的操作)。

        3. 您不想锁定值类型。如果您也真的想要,您可以锁定它的特定装箱,但我想不出除了证明它在技术上是可行的之外,这会带来什么好处 - 它仍然会导致代码不太清楚到底是什么锁关于什么。

        4. 您不想锁定在锁定期间可能会更改的字段,因为您将不再锁定看起来已锁定的内容(这似乎是合理的)这样做的效果有实际用途,但是在代码在第一次阅读时看起来做的事情和实际做的事情之间会有一个阻抗,这永远都不好)。

        5. 必须使用同一个对象来锁定所有可能相互冲突的操作。

        6. 虽然使用过宽的锁可以获得正确性,但使用更精细的锁可以获得更好的性能。例如。如果您有一个保护 6 个操作的锁,并且意识到其中 2 个操作不会干扰其他 4 个操作,因此您更改为拥有 2 个锁对象,那么您可以通过具有更好的一致性(或崩溃和-如果你的分析错了,那就烧吧!)

        第一点排除了锁定任何可见或可以使其可见的东西(例如,就本分析而言,由受保护或公共成员返回的私有实例应被视为公共实例,任何由委托可能会在其他地方结束,依此类推)。

        最后两点可能意味着没有明显的“您正在处理的类型”,因为锁不保护对象,对对象执行的保护操作可能会影响多个对象,或同一个对象受到一组以上必须锁定的操作的影响。

        因此,拥有一个纯粹用于锁定的对象可能是一个好习惯。由于它什么都不做,它不能与其他语义混淆或在你不期望的时候被覆盖。由于它什么都不做,它也可能是 .NET 中存在的最轻量级的引用类型; System.Object.

        就个人而言,我确实更喜欢锁定与操作相关的对象,当它确实符合“您正在处理的类型”的账单时,并且没有其他问题适用,因为在我看来是自我记录,但对其他人来说,做错的风险超过了好处。

        【讨论】:

          猜你喜欢
          • 2011-06-26
          • 2011-12-08
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 2013-03-01
          相关资源
          最近更新 更多