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.