No ThreadStatic Initializer

ID

csharp.no_threadstatic_initializer

Severity

high

Remediation Complexity

trivial

Remediation Risk

low

Remediation Effort

low

Resource

Reliability

Language

CSharp

Tags

concurrency, initialization, reliability, thread-local

Description

Reports a static field marked [ThreadStatic] that also has an initializer. The initializer runs once, on whichever thread first touches the type; every other thread sees the field’s default value instead.

Rationale

A static field initializer is compiled into the type’s static constructor, and the static constructor runs exactly once per type for the whole process. [ThreadStatic], on the other hand, gives the field a separate storage slot per thread. Combining the two means exactly one slot — the one belonging to the thread that happened to trigger type initialization — ever receives the initial value. Every other thread reads 0, false or null.

That makes the defect particularly nasty to find. The thread that triggers type initialization is normally the first thread to run, so single-threaded tests observe the initialized value and pass. The NullReferenceException only appears once a second thread reaches the field, often under load in production.

A [ThreadStatic] field with no initializer is correct: each thread starts from the default and the code lazily fills the slot on first use, per thread. That is the pattern the attribute is designed for.

using System;
using System.Collections.Generic;

public class Buffers
{
    [ThreadStatic]
    private static List<int> pending = new List<int>();     // FLAW — only one thread gets a list

    [ThreadStatic]
    private static List<int> lazy;                          // OK — no initializer

    private static readonly List<int> shared = new List<int>();  // OK — no [ThreadStatic]

    public static void Add(int value)
    {
        if (lazy == null) lazy = new List<int>();           // OK — filled lazily, per thread
        lazy.Add(value);
    }
}

Remediation

Drop the initializer and initialize the slot lazily on first use from each thread, checking for the default value first. If the value must be shared by all threads, remove the [ThreadStatic] attribute instead — the field was never meant to be thread-local.

For anything beyond a trivial default, prefer ThreadLocal<T>: it takes a factory delegate that is invoked once per thread, which expresses "per-thread value with an initializer" directly and removes the null check from every access.