【问题标题】:Using lock(obj) inside a recursive call在递归调用中使用 lock(obj)
【发布时间】:2010-03-09 08:04:51
【问题描述】:

据我了解,在运行时完成 lock(obj) 的代码块之前不会释放锁(因为当块完成时,它会调用 Monitor.Exit(obj)。

有了这种理解,我无法理解以下代码行为背后的原因:

private static string obj = "";
        private static void RecurseSome(int number)
        {
            Console.WriteLine(number);
            lock (obj)
            {
                RecurseSome(++number);
            }
        }

//致电: RecurseSome(0)

//输出: 0 1 2 3...... stack overflow exception

一定有一些我遗漏的概念。请帮忙。

【问题讨论】:

    标签: c# .net multithreading locking


    【解决方案1】:

    锁知道哪个线程锁定了它。如果同一个线程再次出现,它只会增加一个计数器并且不会阻塞。

    因此,在递归中,第二个调用也进入了 - 并且锁在内部增加了锁计数器 - 因为它是同一个线程(已经持有锁)。

    ms-help://MS.VSCC.v90/MS.MSDNQTR.v90.en/dv_csref/html/656da1a4-707e-4ef6-9c6e-6d13b646af42.htm

    或 MSDN:http://msdn.microsoft.com/en-us/library/c5kehkcz.aspx

    状态:

    lock 关键字确保一个线程不进入代码的临界区而另一个线程 处于临界区。如果另一个线程试图输入一个锁定的代码,它将等待、阻塞,直到对象被释放。

    注意线程引用和对“另一个”线程的强调。

    【讨论】:

      【解决方案2】:

      锁定字符串对象。这可能会导致意外行为,例如应用程序中的死锁。您当前正在锁定空字符串,这更糟。整个程序集使用相同的空字符串。让事情变得更糟;作为一种优化,CLR 在 AppDomain 上重用字符串。锁定字符串意味着您可能正在执行跨域锁定。

      使用以下代码作为锁对象:

      private readonly static object obj = new object();
      

      更新

      事实上,我认为可以肯定地说,允许锁定任何东西是 .NET 框架中的一个主要设计缺陷。相反,他们应该创建某种SyncRoot 密封类,并且只允许lock 语句和Monitor.Enter 接受SyncRoot 的实例。这会为我们省去很多痛苦。我确实知道这个缺陷来自哪里; Java 也有同样的设计。

      【讨论】:

      • +1 是唯一一个选择这个的人。字符串可以被实习,这意味着奇怪的行为。
      • 这尤其意味着不受控制的行为——完全正确。使用任何东西(对象,分配一个 Guid,一个 int,任何东西)而不是一个字符串。
      • @TomTom:不要使用 Guid 或 int!仔细考虑将它们传递给 Monitor.Enter 时会发生什么。
      • @Steven:我同意你的观点,锁定任何东西的能力,以及随之而来的用于创建指向从未使用过的同步块的指针的内存使用量的增加,具有严重的缺陷。我不知道我会说这是一个重大缺陷,但我同意你的观点。你对根本原因的理解是正确的;当您意识到 CLR 设计者希望能够支持类 Java 行为的超集时,CLR 有几个有问题的设计选择更有意义。这也是我们得到不安全数组协方差的地方。
      • 不应将不锁定字符串对象的建议视为绝对命令。在某些情况下,锁定实习字符串可以使您不必手动管理互斥对象。我在这里谈论这个:stackoverflow.com/a/29878495/64334
      【解决方案3】:

      正如其他人已经指出的那样,锁由线程持有,因此可以工作。但是,我想对此添加一些内容。

      Joe Duffy,来自微软的并发专家,有多个关于并发的设计规则。他的设计规则之一是:

      9 .在您的设计中避免锁递归。使用非递归锁 在可能的情况下。

      递归通常表明你的 同步设计,经常 导致不太可靠的代码。一些 设计使用锁递归作为一种方法 避免将功能拆分成那些 带锁的和假设的 锁已经被占用。这个可以 诚然导致代码减少 尺寸,因此更短 写时间,但结果更多 最终的脆弱设计。

      (source)

      为防止递归锁,将代码改写如下:

      private readonly static object obj = new object();
      
      private static void Some(int number)
      {
          lock (obj)
          {
              RecurseSome(number);
          }
      }
      
      private static void RecurseSome(int number)
      {
          Console.WriteLine(number);
          RecurseSome(++number);
      }
      

      此外,我的代码会抛出StackOverflowException,因为它永远不会结束递归调用自身。你可以重写你的方法如下:

      private static void RecurseSome(int number)
      {
          Console.WriteLine(number);
          if (number < 100)
          {
              RecurseSome(++number);
          }
      }
      

      【讨论】:

      • +1 获取递归锁设计的提示。我以前从来没有这样想过 - 将在以后的代码中记住这一点。
      【解决方案4】:

      锁归当前线程所有。递归调用也在当前线程上进行。如果另一个线程试图获取锁,它会阻塞。

      【讨论】:

        【解决方案5】:

        如果您要询问堆栈溢出异常 - 这是因为那里没有任何东西可以从递归中中断。堆栈空间通常只有几 K,您很快就会耗尽空间。

        现在这种情况下的锁可用于序列化调用的输出,这样如果您从两个不同的线程调用 RecurseSome,您将看到来自第一个线程的整个列表,然后是来自第二个线程的整个列表线。如果没有锁,两个线程的输出就会交错。

        通过拆分方法,您可以在不递归获取锁的情况下获得相同的结果:

        private static void RecurseSome(int number)
        {
            lock (obj)
            {
                RecurseSomeImp(number);
            }
        }
        
        private static void RecurseSomeImp(int number)
        {
            Console.WriteLine(number);
            if( number < 100 ) // Add a boundary condition
                RecurseSomeImp(++number);
        }
        

        这实际上会表现得更好,并且获取和释放锁很快,但不是免费的。

        【讨论】:

          【解决方案6】:

          与锁无关。检查您的递归代码。停止递归的边界情况在哪里?

          【讨论】:

          • 投反对票的人能解释一下吗?他在问stackoverflow的原因[没有双关语]。无论使用该代码的锁如何,它都会发生。所以锁是红鲱鱼。
          • 很明显他的代码是用来证明问题的。他认为它不应该递归,而是在递归时阻塞。
          • 这并不明显。 >>>以下代码行为背后的原因。行为 = stackoverflow 原因 = 没有边界情况。如果他只关心锁的重入,他可以使用 lock(obj);lock(obj);或 f(){lock(obj){}; g();} g(锁(obj){});为什么要引入递归[也没有边界情况]作为红鲱鱼。也看看别人的回复。他们中的许多人都有同样的困惑。
          • @Fakrudeen 您说他在询问stackoverflow 的原因,是在把话放在OP 的嘴里。 OP 明确表示他正在质疑对锁定行为而不是无限循环的理解。问题的标题也很清楚,它是关于递归的。这就是你被否决的原因。
          【解决方案7】:

          这是一个连续循环,因为无法确定何时停止递归,并且线程试图访问的对象总是被阻塞。

          【讨论】:

          • 自相矛盾。
          猜你喜欢
          • 1970-01-01
          • 2015-08-14
          • 2016-08-07
          • 1970-01-01
          • 2016-03-21
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 2023-04-03
          相关资源
          最近更新 更多