如何安全地解决BeforeFieldInit和静态构造函数周期?

时间:2019-03-15 20:55:51

标签: c# .net deadlock static-initialization static-constructor

我担心以下两种行为之间的相互作用:

http://www.ecma-international.org/publications/files/ECMA-ST/ECMA-335.pdf#page=179

  

2.1。如果类型尚未初始化,请尝试获取初始化锁。

     

2.2.1。如果未成功,请查看该线程还是等待该线程完成的任何线程已经持有该锁。

     

2.2.2。如果是这样,则返回,因为阻塞将导致死锁。现在,该线程将看到该类型的未完全初始化状态,但是不会出现死锁。

http://www.ecma-international.org/publications/files/ECMA-ST/ECMA-335.pdf#page=69

  

如果标记为BeforeFieldInit,则在首次访问为该类型定义的任何静态字段时或之前执行该类型的初始化方法。

此代码示例演示了可能的交互:

static class Foo {
  public static int A = 1;
}
static class Bar {
  public static int B = Foo.A;
}
static class Program {
  static void Main() {
    Console.WriteLine(Bar.B);
  }
}

在任何理智的环境中进行测试时,它将输出1。但是,似乎规范允许通过执行以下操作来输出0:

  1. 主程序开始执行。
  2. Foo的类型初始值设定项开始执行(由于BeforeFieldInit规则,目前允许这样做)。
  3. Bar的类型初始值设定项开始执行(由于BeforeFieldInit规则,目前允许这样做)。
  4. Bar.B的初始化程序开始执行。
  5. 请求Foo.A。
  6. Foo的类型初始化器已在运行,等待它会导致死锁。死锁规则允许我们以未完全初始化的状态查看Foo,其中A尚未设置为1,但其默认值为0。
  7. Bar.B设置为0。
  8. Bar的类型初始化程序已完成。
  9. Foo.A设置为1。
  10. Foo的类型初始化程序已完成。
  11. 主输出Bar.B,为0。

这真的允许吗?我应该如何编写类型初始值设定项,以免被它咬住?

1 个答案:

答案 0 :(得分:3)

  

这真的允许吗?

肯定看起来像是规范所允许的。

  

在任何理智的环境中进行测试时,它将输出1。

是的。了解优化背后的原因是有帮助的。 “松弛语义”的目的是移动对“静态构造函数是否运行?”的检查。从访问类型的执行时间访问类型的方法的时间。也就是说,如果我们有:

void M()
{
    blah
    if blah blah
    ... Foo.A ...
    if blah blah blah
    ... Foo.A ...
    blah blah blah
}

假设现在是M的准时,并且Foo的cctor尚未执行。为了严格遵守,抖动必须在每次访问Foo.A 时生成代码,以检查Foo cctor是否已经执行,如果尚未执行,则执行该代码。

但是,如果我们在准时执行cctor调用 ,则 jitter 就会知道Foo是在M内部访问的,因此可以调用当M被抖动时,返回cctor,然后跳过在M内部生成每个检查。

在jit时执行cctor时,抖动足够聪明,可以执行正确的操作;它不会按照您所描述的那样以“错误”的顺序执行cctor,因为编写抖动的人是理智的人,他们只是想使您的代码更快。

  

我应该如何编写类型初始值设定项,以免被它咬住?

您应该假设符合实现的作者是理智的。

如果出于某种原因您不能假设:您可以将所有您关心的静态字段初始化器放入静态构造函数中。 C#编译器不允许在具有静态构造函数的类型上使用BeforeFieldInit语义。