0

My class has a static field initializer which is used in the constructor:

class Foo {
  private static List<string> list = new List<string>()
  private static object listLock = new Object();

  public Foo(string s) {
    lock(listLock)
      list.Add(s);
  }
}

My problem is, that it occasionally occurs, that the list is accessed in the constructor before the static initializer has finished, causing a NullReferenceException when accessing list. According to this question, it is only guaranteed that static initialization is started before an instance can be created, but not that it is finished.

Is there some way to ensure that the constructor is only called after the static one has finished (apart from ugly hacks like while(list == null){})?

mat
  • 1,645
  • 15
  • 36
  • 1
    @DanielA.White that's... not quite true :) ah, good ol' before-field-init – Marc Gravell Jun 12 '19 at 19:36
  • 1
    Shouldn't this work if you just used an explicit static constructor? – Camilo Terevinto Jun 12 '19 at 19:37
  • https://stackoverflow.com/questions/53018370/why-the-default-constructor-executed-before-the-static-constructor\ – Daniel A. White Jun 12 '19 at 19:40
  • The static initializer and the instance constructor are both running in the same thread. So, provided that the static initializer does not create instances of the class, the static initializer will finish before the start of any instance constructor. – Theodor Zoulias Jun 12 '19 at 19:50
  • @TheodorZoulias: This is not correct for initializers, as my example shows. But it works if I create an explicit static constructor as suggested in [this answer](https://stackoverflow.com/a/56569318/1039462) – mat Jun 12 '19 at 21:21
  • @mat you mean that you are getting null-reference exceptions from the code you posted? – Theodor Zoulias Jun 12 '19 at 21:34
  • @TheodorZoulias Yes, exactly. I edited the question to make that more clear. – mat Jun 13 '19 at 07:37
  • Does actually `while(list == null){}` solves the problem, or it's just a guess? – Theodor Zoulias Jun 14 '19 at 00:49
  • @TheodorZoulias I haven't tried it. It was a dirty hack coming to my mind, but I wanted to wait for a better solution on SO first before implementing it. Luckily, [such a solution](https://stackoverflow.com/a/56569318/1039462) was provided an it works great. – mat Jun 17 '19 at 15:49

2 Answers2

3

Here's a good link on the nuances of before-field-init, but indeed: adding an explicit static constructor should force the runtime's hand here. Note that the while (list == null) {} will not work, because that also forces the runtime's hand - you should never be able to observe the lies that the runtime tells you, basically.

In the example shown, it won't matter. You will never see a null for listLock or list. If you really really demand that they run before constructors:

private static List<string> list;
private static object listLock;
static Foo() {
    list = new List<string>();
    listLock = new object();
}

But note that this is not really necessary and may negatively impact your code, especially with the new JIT in .NET Core 3 that can treat static readonly fields with extra voodoo if they are initialized neatly (I know it can do this if they are inline field initializers without an explicit static constructor; I don't know if it can do this if they are assigned by an explicit static constructor).

Marc Gravell
  • 1,026,079
  • 266
  • 2,566
  • 2,900
  • Well, I see a null value for list sometimes, that's why I discovered that there is a problem. – mat Jun 13 '19 at 09:04
  • @mat mark it as `readonly`; I strongly suspect that some piece of code is changing it; unless you've broken the runtime (which, to be fair *can happen*, but it is exceptionally rare): you won't be viewing it as null because of an initialization race – Marc Gravell Jun 13 '19 at 14:57
0

I can't reproduce the issue. The program below creates 5 Foo instances in parallel, and the constructor of each Foo instance uses as a lock a static instance of a class SlowObject, that needs 500 msec for its instantiation. Nevertheless the SlowObject is always created before any Foo constructor is started. I tested this program with various versions of .NET Framework, C# versions 4, 5, 6 an 7, with both Debug and Release configurations, and with and without debugger attached. The output is always the same. Commenting out the static constructors makes no difference.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using System.Threading;

class Program
{
    static Program()
    {
        Program.ConsolePrint("Program Static Constructor");
    }

    static void Main(string[] args)
    {
        ConsolePrint("Starting Tasks");
        var tasks = Enumerable.Range(1, 5).Select(n => Task.Run(() =>
        {
            new Foo(n.ToString());
        })).ToArray();
        ConsolePrint("Waiting Tasks");
        Task.WaitAll(tasks);
        ConsolePrint("Tasks Finished");
        //Console.WriteLine("Foo.list: " + String.Join(", ", Foo.GetItems()));
    }

    public static void ConsolePrint(string line)
    {
        Console.WriteLine(DateTime.Now.ToString("HH:mm:ss.fff") + " ["
            + Thread.CurrentThread.ManagedThreadId.ToString() + "] > " + line);
    }
}

public class Foo
{
    static Foo()
    {
        Program.ConsolePrint("Foo Static Constructor");
    }

    private static List<string> list = new List<string>();
    private static object listLock = new SlowObject();

    public Foo(string s)
    {
        Program.ConsolePrint("Creating Foo " + s);
        lock (listLock)
        {
            list.Add(s);
        }
    }
}

public class SlowObject
{
    static SlowObject()
    {
        Program.ConsolePrint("SlowObject Static Constructor");
    }

    public SlowObject()
    {
        Program.ConsolePrint("SlowObject Instance Constructor Started");
        Thread.Sleep(500);
        Program.ConsolePrint("SlowObject Instance Constructor Finished");
    }
}

Output:

13:40:24.091 [1] > Program Static Constructor
13:40:24.112 [1] > Starting Tasks
13:40:24.131 [1] > Waiting Tasks
13:40:24.132 [3] > SlowObject Static Constructor
13:40:24.133 [3] > SlowObject Instance Constructor Started
13:40:24.635 [3] > SlowObject Instance Constructor Finished
13:40:24.635 [3] > Foo Static Constructor
13:40:24.637 [3] > Creating Foo 1
13:40:24.637 [4] > Creating Foo 2
13:40:24.643 [3] > Creating Foo 5
13:40:24.639 [5] > Creating Foo 3
13:40:24.641 [6] > Creating Foo 4
13:40:24.647 [1] > Tasks Finished
Press any key to continue . . .
Theodor Zoulias
  • 34,835
  • 7
  • 69
  • 104
  • It was not a deterministic problem, only happened from time to time – mat Jun 13 '19 at 20:41
  • The non-deterministic nature of the problem indicates that there is a race condition somewhere in the program. In this case it should be possible to exaggerate the problem by adding artificial delays. My tests indicate that the order of execution is well defined, and the static initializers must complete before any instance constructor can be started. Honestly I am inclined to believe that something else is going wrong in your case. – Theodor Zoulias Jun 14 '19 at 00:57