Thread-local storage

static threadlocal gives each native thread a separate value of one static field.

Declaring a thread-local field

threadlocal is a field modifier and is valid only together with static. Ordinary locals are already separate for every function invocation and do not need it.

struct WorkerState
{
    public static threadlocal int WorkerId;
}

WorkerState.WorkerId = 7;

A write changes only the calling native thread's value. Another thread sees its own zero-initialized or lazily initialized value. This also applies to threads created outside Xenon that enter exported Xenon code.

Lazy initialization

An explicit initializer runs once per native thread, immediately before that thread's first access to the field. A thread that never accesses the field does not run its initializer and pays no per-field initialization cost.

struct ScratchState
{
    private static threadlocal unique<int[]> Scratch
        = new int[1024];
}

Later accesses on the same thread reuse the stored value. Initializers may use normal runtime expressions, including function calls and allocation. For a value that requires destruction, the first access initializes the per-thread instance and registers its thread-exit cleanup. A thread that never accesses the field runs neither the initializer nor the cleanup registration. Recursively accessing the same field while its initializer is still running is invalid and terminates through the normal runtime trap.

Storage is not ownership

threadlocal changes where and for how long a field value is stored. It never changes the field's type and never creates ownership.

static threadlocal int[] Buffer = new int[256];
static threadlocal Job* Current;
static threadlocal unique<int[]> OwnedBuffer = new int[256];

Buffer and Current are unmanaged. Xenon does not call free for them at thread exit; the program remains responsible for their allocation lifetime. OwnedBuffer has normal unique ownership, so its allocation is released by normal ownership destruction.

Destruction at thread exit

If an accessed TLS value has a destructor or ownership cleanup, Xenon registers that cleanup with the current native thread. Only values initialized on that thread are destroyed. Cleanup uses the same rules as an ordinary value: user destructors run, unique<T> values release their allocation, shared<T> values release strong ownership, and weak<T> values release weak ownership. A struct is non-trivial when it declares a destructor or contains a value that requires destruction.

static threadlocal unique<Foo> UniqueValue;
static threadlocal shared<Foo> SharedValue;
static threadlocal weak<Foo> WeakValue;
static threadlocal MyStruct StructValue;

When several destructible TLS fields are first initialized on one thread, Xenon destroys them in reverse initialization order. This behavior does not require Xenon to create or own the native thread.

Windows uses the platform FLS cleanup facility. Linux and macOS use a lazily allocated pthread TLS key and do not require a C++ ABI runtime. Programs without destructible TLS do not acquire this native threading-runtime link requirement.

Synchronization is separate

Thread-local storage prevents different threads from sharing this field value. It does not make objects reached through pointers thread-safe. Use atomic values where threads intentionally share mutable state, and see the memory model for data-race rules.

Native module lifetime and TLS

A Xenon native module containing initialized non-trivial threadlocal storage must remain loaded until every native thread that accessed and initialized that TLS has terminated. Thread-exit cleanup callbacks execute code from the module that defined the TLS. Unloading its DLL, .so, or .dylib first leaves the operating system with a callback into unloaded code and is invalid behavior.

This rule also applies to threads created outside Xenon, including C threads, C++ std::thread, pthread and Win32 threads, .NET native-host workers, Unity or native-plugin workers, and threads created by another host runtime. Xenon does not need to own thread creation for lazy initialization or thread-exit destruction to work.

The required host lifecycle is:

Load Xenon library
    ↓
Start native workers
    ↓
Workers call Xenon and may initialize non-trivial TLS
    ↓
Join or terminate every such worker
    ↓
Unload Xenon library

The restriction is per initialized, non-trivial TLS instance. A thread that never accesses the field does not initialize it and does not register its cleanup. Trivial values and unmanaged pointers or arrays do not gain ownership merely because they are threadlocal, so Xenon does not register ownership destruction for them.

static threadlocal int Count;
static threadlocal bool Ready;
static threadlocal void* Context;
static threadlocal Foo* Current;
static threadlocal int[] Buffer = new int[1024];

In particular, Xenon does not automatically call free(Buffer) at thread exit. Use an ownership type such as unique<int[]> when automatic release is required. The unload restriction applies when accessing a field causes Xenon to register thread-exit destruction, not to every thread or every TLS field in the process.