Are standard C# static class static constructors thread safe when calling UnityEngine API?

If I have a standard C# static class is it safe to call the UnityEngine API in the static constructor?

As an example would this be thread-safe:

public static class TestStaticClass
    public static GameObject Test;

    static TestStaticClass()
         Test = Resources.Load<GameObject>("file.asset");

I created a little debug process that saves the thread object during Unity Awake() of my first GameGame object to run. In all the standard C# static constructors I check to see if the thread is the same thread as the main start thread. So far, it always looks like the static constructor is run on the main Unity thread. Yet I have read many places that Unity can run C# static constructors on a thread other than the main Unity thread which would make UnityEngine API calls unsafe in the constructors. Is it unsafe to trust these C# static constructors based on my test? Testing for thread issues is hard. If my thread comparison is accurate than what I have read about Unity using another thread for C# static constructors is wrong. Maybe I have read things too loosely? Maybe only instance constructors are unsafe to call UnityEngine API? Maybe C# constructors within MonoBehavoir children are unsafe? I would love some definitive clarity.

Safe? Not necessarily and most likely: No. The issue is not that the static constructor is not thread-safe. because it is thread-safe. However the static constructor (or type initializer) is executed whenever the class / type is first accessed from whatever thread that might be. The Unity API can only be used from the main thread. If the first access to your class happens from a different thread, it would not work.

Also relying on a particular type initialization order is generally a bad idea. By just adding a sinple read access to a static field of your class at the wrong point would suddenly break your code. So don't use the static constructor for anything like that unless you can ensure when the type is accessed for the first time. The only case when this might be the case is a nested private static class inside a MonoBehaviour.

It's usually better to use properties to lazy initialize those fields as they are needed. Lazy initialization has a similar issue, however it's limited to the actual property and is not bound to the type initialization. For example it's possible that some reflection code could trigger the type initialization.

Though the best solution is to provide an Init or Load method which you specifically call from the main thread at some point. So you have full control over when the code executes. If some code might access one of those fields too early it's actually good to get a NullRefException, because it shows that you have an ordering issue.

1 Like

The RuntimeInitilializeOnLoadMethod attribute may be what you're looking for.
Add this attribute to any static method and Unity will automatically call it after the Awake lifecycle.

1 Like

@Bunny83 Thanks! You have confirmed what I have been thinking/finding.

Find Unity to be somewhat of a paradox. There are a lot of concepts like Start/Awake that push you to think in terms of all objects taking responsibility for their own startup. Then there is the lack of engine thread safety and constructors running on multiple threads that push you to think of a unified startup process where you completely control the startup but where you have to make sure new stuff gets added during updates. There are pros and cons to both lines of thought and clearly one approach does not rule them all.

@Vryken I looked at RuntimeInitializeOnLoadMethod and it seems pretty powerful. It just sidestepped the threading question a bit. Does Unity run a bunch of RuntimeInitializeOnLoadMethod marked methods on different threads so that calling UnityEngine API in them would be unsafe? I noted there example only calls Debug.Log which I found stated somewhere is thread-safe. I really wish that the documentation added a lot more notes about threading. I was disappointed to find that the new job scheduling system is run on the multi-threads so you can't use UnityEninge calls in the jobs.

I am sure it would be a lot of effort but a version of the UnityEngine that could be called on non-main threads would be awsome.