为什么在这种情况下没有死锁?

10

我正在愉快地阅读Eric Lippert的这篇文章,当然还有其中很好的评论,在其中John Payson说:

更有趣的例子可能是使用两个静态类,因为这样的程序可以在没有任何可见的阻塞语句的情况下发生死锁。

我想,是啊,那很容易实现,所以我写了这个东西:

public static class A
{     
    static A()
    {
        Console.WriteLine("A.ctor");
        B.Initialize();
        Console.WriteLine("A.ctor.end");
    }

    public static void Initialize()
    {
        Console.WriteLine("A.Initialize");
    }
}
public static class B
{
    static B()
    {
        Console.WriteLine("B.ctor");
        A.Initialize();
        Console.WriteLine("B.ctor.end");
    }

    public static void Initialize()
    {
        Console.WriteLine("B.Initialize");
    }

    public static void Go()
    {
        Console.WriteLine("Go");
    }
}

调用 B.Go() 后的输出为:

B.ctor
A.ctor
B.Initialize
A.ctor.end
A.Initialize
B.ctor.end
Go

没有死锁,我显然是个失败者-为了让尴尬永恒,这里是我的问题:为什么这里没有死锁?

在我微小的大脑中,似乎在B的静态构造函数完成之前调用了B.Initialize,我认为这是不允许的。


在每个点上查看堆栈跟踪可能会很有趣。 - leppie
你如何提出让一个线程进入死锁状态的建议?所有事情都在同步发生... - Ed S.
5个回答

6
这并不是死锁,因为您没有执行任何应该导致阻塞的操作,也没有执行应该破坏的操作。
在A和B之间,您没有使用任何资源,因此您的循环依赖性在“安全”意义上是可靠的,因为不会出现任何问题。
如果你跟踪你的打印输出所显示的路径,那么没有什么应该会被阻止:
1.调用 Go(我猜测) 2.进入 B(静态构造函数),因为它尚未初始化。 3.打印输出 4.使用 A.Initialize() 5.A的静态构造函数需要先执行 6.打印输出 7.使用 B.Initialize() 8.B不需要初始化,但它不处于已完成状态(幸运的是没有设置变量,因此不会发生错误) 9.打印输出,然后返回 10.打印输出(来自A的静态构造函数),然后返回 11.A.Initialize()最终可以被调用,因为A已经初始化了 12.打印输出,然后返回 13.打印输出(来自B的静态构造函数),然后返回 14.Go
你真正做的唯一的事情就是呈现了一个不安全的状态: 访问一个构造函数尚未完成执行的类。这是不安全的代码,虽然不会导致阻塞,但绝对代表了一个损坏的状态。

1
重要的一点是只涉及一个线程。引用自博客文章:
静态构造函数然后启动一个新线程。当该线程启动时,CLR 看到一个静态方法即将在其静态构造函数“正在运行”的类型上调用另一个线程。它立即阻塞新线程,以便 Initialize 方法不会开始,直到主线程完成运行类构造函数。
在 Eric 的示例中,有两个线程相互等待。您只有一个线程,因此没有等待发生,因此也没有阻塞和死锁。

啊,是的,这就是为什么我很蠢 - 当然我不能死锁单个执行线程 - 我只能像@pickypg所说的那样,产生一个不安全的情况。谢谢。 - kmp
静态构造函数在新线程中执行和不执行的情况是否清楚? - Andrew Savinykh
@zespri:请提供一个样例。 - Daniel Hilgarth
@DanielHilgarth:抱歉,你在说什么?示例是什么? - Andrew Savinykh
@zespri:当您说“在新线程中执行静态构造函数时”的任何情况的示例。 - Daniel Hilgarth
显示剩余2条评论

1

为什么您认为会出现死锁。静态构造函数只会被调用一次。无论您执行多少次语句B.Initialize();,它都只会在第一次引用B时调用类B的静态构造函数。在这里了解更多关于静态构造函数的信息here


0
静态方法只会被调用一次,一旦加载后就不会再次调用。 如果A.Initialize()方法调用了B.Initialize()方法,反之亦然,则会出现死锁。
因此,任何类首次加载到内存中时,静态块都会被执行,之后对它的任何后续调用 - 因为该类已经加载,所以静态块不会被执行。

如果A.Initialize调用了B.Initialize,反之亦然,那么这不会导致死锁,而是会引发StackOverflowException异常,对吗? - kmp
由于这两个类没有共同的资源,所以不存在死锁的概念 - 唯一可能发生的事情就是填满堆栈。 - Shiva Komuravelly
这显然是递归,但没有死锁。 - Alvin Wong

-3

类实例在构造函数调用之前实际上已经被创建,因此不需要再执行构造函数了。


我们首先在哪里创建对象以执行构造函数 - 因为我们仅使用静态方法和静态块,所以没有构造函数的概念。 - Shiva Komuravelly

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接