Something to be aware of with static members, especially when using a singleton pattern #3218
dodexahedron
started this conversation in
General
Replies: 1 comment 1 reply
-
Good post. Thanks. We've lived this, so know exactly what you mean. |
Beta Was this translation helpful? Give feedback.
1 reply
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
-
Static members of types can have some surprising and unintuitive behavior. That behavior can also be non-deterministic between executions of the same application on the same machine, even one right after another.
TL;DR of the below stuff: If you have a singleton, or any other non-trivial static member in a type, include an explicit static constructor, even if it's literally just empty.
What can happen and why?
Well, let's start with some of the why, which is explained in less detail in the docs and which I'll expand on here:
There's a bit of metadata the compiler emits for a class that does not have an explicit static constructor, even if there are static members that have initializers. It's the beforeFieldInit flag, which isn't something that can be directly set on a type, except via putting a static constructor in it. The only ways I know of for you to actually see it are to reflect on the type and look for the BeforeFieldInit value in the Attributes, or to actually look at the IL.
What is the impact?
Well, if the beforeFieldInit flag is present on a type because you didn't put an explicit static constructor in it, access to static members of the type, no matter what they are, is not guaranteed to actually initialize the type. It's a kind of optimization that works just fine when those static members are just methods without dependencies on other static members of the type, but you lose a lot of guarantees in many other situations.
It is a thread safety heisenbug, too, with singletons and certain other situations, because, without the type being initialized, even if you null check your singleton before assignment, two different callers can attempt to create the singleton at the same time, and one of them is going to be lost as unreferenced garbage, with undefined behavior depending on what happens after that in the loser's code.
You also still have no guarantee that, when that singleton instance is created, any of the other static members have been or will be initialized when they are first referenced, with the exception of static fields, which are at least initialized to their default values (which is quite a shock when you have a field initializer on that field and you don't get its value back, every now and then).
Are there any drawbacks to having even an empty static constructor?
A (usually) small one, but it does exist. The entire reason that beforeFieldInit flag is emitted by default is because it is an easy and useful optimization that the JIT compiler can make, for a wide range of cases. So, if one is there, and that flag is therefore not emitted, that little boost is lost, and all of the field and property initializers WILL be run, BEFORE the static constructor, then the static constructor will run, and that's all automatically done just before the first use of any part of the type (even non-static parts).
Another benefit, though, to just putting that constructor in there, is that the runtime automatically locks the type while the constructor is running, making it and all the other static initializers in the type implicitly thread-safe, without you needing to do anything else.
And, of course, singletons should pretty much always be declared
readonly
, but I'm pretty sure everyone already knew that one.Whatever you do, take every possible precaution to keep a static constructor or any of the static field initializers from being able to throw an exception, because it's non-recoverable if it does happen. Why's that? A static constructor will only be called up to once per lifetime of an AppDomain. This includes if it fails during that attempt. So, if it fails, it'll never happen again, and the only way to make it run again is to restart the whole AppDomain.
There are some other benefits of using static constructors, too, for types that have various static fields. If you initialize those fields in the constructor instead of with initializers, you can control the execution order of that initialization. That can actually be mandatory, sometimes, depending on what those members are and how they get their values. The compiler will sometimes warn you about it, if it sees a potential execution order issue with static members that initialize using initializers, but it misses plenty of situations because it's not possible to know ahead of time what order they'll run in, at run-time, unless you do it yourself in the static constructor.
Programming is fun!
Beta Was this translation helpful? Give feedback.
All reactions