在递归调用中使用lock(obj)

时间:2010-03-09 08:04:51

标签: c# .net multithreading locking

根据我的理解,在运行时完成锁(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

必须有一些我缺少的概念。请帮忙。

7 个答案:

答案 0 :(得分:35)

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

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

MS-帮助://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关键字确保一个线程不会进入代码的关键部分,而另一个线程位于关键部分。如果另一个线程试图输入锁定的代码,它将等待,阻止,直到该对象被释放。

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

答案 1 :(得分:35)

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

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

private readonly static object obj = new object();

更新

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

答案 2 :(得分:9)

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

来自Microsoft的并发专家

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);
    }
}

答案 3 :(得分:6)

锁定由当前线程拥有。递归调用也是在当前线程上进行的。如果另一个线程尝试获取锁定,它将阻止。

答案 4 :(得分:1)

如果你问的是堆栈溢出异常 - 那是因为那里没有任何东西可以打破递归。堆栈空间通常只有几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);
}

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

答案 5 :(得分:0)

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

答案 6 :(得分:0)

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