ThreadStatic is applied to an instance field

ID

vbnet.correctness.thread_static_instance_field

Severity

low

Remediation Complexity

trivial

Remediation Risk

low

Remediation Effort

low

Resource

Concurrency

Language

VB.NET

Description

Reports a field declaration carrying <ThreadStatic> (or <ThreadStaticAttribute>) that is not declared Shared. Both the plain and the initialised declaration forms are reported, under any access modifier. A Shared field with the same attribute is the supported use and is not reported.

Rationale

<ThreadStatic> has meaning only for a shared field: it asks the runtime to give each thread its own copy of the single storage location that backs that field. An instance field already has one storage location per object, so there is nothing for the attribute to partition and the runtime ignores it outright - no error, no warning, no thread-local behaviour. The field keeps exactly the semantics it would have had if the attribute were deleted, which is why the mistake survives review: the declaration reads as thread-local and behaves as ordinary shared-by-object state. Code written on the strength of the attribute is then wrong in the way that is hardest to reproduce. A tracer that keeps its nesting depth in an attributed instance field, reached through an object several threads hold a reference to, has every thread incrementing and resetting the same counter; the depth is plausible under a single request and drifts under load. Because the instance is often long-lived and the interleaving is timing-dependent, the symptom appears as occasional corrupt output rather than as a failure that points at the declaration.

The following code illustrates the pattern detected by this rule:

Public Class RequestTracer

    ' FLAGGED: ThreadStatic is applied to an instance field
    <ThreadStatic>
    Private currentDepth As Integer

    ' FLAGGED: ThreadStatic is applied to an instance field
    <ThreadStatic>
    Private Protected activeScope As String = "root"

    ' FLAGGED: ThreadStatic is applied to an instance field
    <ThreadStaticAttribute>
    Friend correlationId As Guid

    Public Sub Enter(scope As String)
        currentDepth += 1
        activeScope = scope
    End Sub

Remediation

Decide which of the two the field is meant to be. If it really is per-thread state, add Shared so the attribute takes effect - and remember the field is then one value per thread for the whole process, initialised to the type default on each new thread, because a field initialiser on a <ThreadStatic> field runs only on the thread that first touches the class. If the field is genuinely per-object state, delete the attribute; where the intent was per-thread state that still needs to be reachable per instance, hold a ThreadLocal(Of T) in an ordinary instance field instead, which keeps the storage tied to the object and gives each thread its own value with a proper initialiser.

' Before: the attribute is ignored; every thread shares this object's counter
Public Class RequestTracer
    <ThreadStatic>
    Private currentDepth As Integer
End Class

' After: per-thread state, kept per instance and properly initialised
Public Class RequestTracer
    Private ReadOnly currentDepth As New ThreadLocal(Of Integer)(Function() 0)
End Class

Configuration

This detector does not need any configuration.