How to Declare Thread_local Variables in C

Thread-local storage allows concurrent threads to maintain separate, independent instances of a variable rather than sharing a single global or static memory location. Introduced in the C11 standard, the _Thread_local storage-class specifier provides native language support for creating variables that are tied to the lifecycle of an individual thread. This article covers how to declare thread-local variables in C, the syntax requirements for different scopes, and best practices for safe implementation.

Syntax and the <threads.h> Header

In standard C11 and later, thread-local storage is declared using the _Thread_local keyword. Alternatively, including the <threads.h> header defines a convenient macro, thread_local, which expands to _Thread_local.

#include <threads.h>

// Using the macro from <threads.h>
thread_local int counter = 0;

// Using the raw keyword
_Thread_local int raw_counter = 0;

In C23, thread_local is recognized as a standard keyword without requiring <threads.h>.


Scope and Storage Rules

The _Thread_local specifier can only be applied to variables with thread storage duration. It cannot be applied to functions or function parameters. The rules for applying _Thread_local depend on the scope in which the variable is declared:

1. File Scope (Global Variables)

At file scope (outside of any function), _Thread_local can be used alone, or combined with static or extern:

  • External linkage: Declaring _Thread_local int global_var; makes the variable visible across translation units, but each thread across the entire program receives its own separate instance.
  • Internal linkage: Combining it as static _Thread_local int file_var; restricts visibility to the current translation unit while maintaining a unique copy per thread.

2. Block Scope (Local Variables)

Inside a function or block, _Thread_local must be combined with either static or extern. Attempting to declare an automatic (ordinary local) variable as _Thread_local will result in a compiler error.

void process_data(void) {
    // Valid: static local variable with thread duration
    static _Thread_local int invocation_count = 0;

    // Valid: refers to a thread-local variable defined elsewhere
    extern _Thread_local int global_status;

    // Invalid: causes a compilation error
    // _Thread_local int auto_var = 10; 
}

Lifecycle and Initialization

  • Initialization: A thread-local variable is initialized once per thread when the thread starts. If an explicit initializer is provided, it must be an expression that evaluates to a constant at compile time. Uninitialized thread-local variables are zero-initialized by default.
  • Destruction: The storage associated with a thread-local variable persists until the corresponding thread terminates. Once the thread exits, its local copies are automatically deallocated.

Code Example

Below is a complete example demonstrating distinct thread-local storage across two threads:

#include <stdio.h>
#include <threads.h>

thread_local int thread_specific_id = 0;

int worker(void *arg) {
    int id = *(int *)arg;
    thread_specific_id = id; // Modifies only this thread's copy
    
    thrd_sleep(&(struct timespec){.tv_nsec = 100000000}, NULL);
    
    printf("Worker Thread ID: %d (Stored Value: %d)\n", id, thread_specific_id);
    return 0;
}

int main(void) {
    thrd_t t1, t2;
    int id1 = 1, id2 = 2;

    thread_specific_id = 0; // Main thread's copy

    thrd_create(&t1, worker, &id1);
    thrd_create(&t2, worker, &id2);

    thrd_join(t1, NULL);
    thrd_join(t2, NULL);

    printf("Main Thread Value: %d\n", thread_specific_id); // Remains 0
    return 0;
}

Key Considerations

  • Pointers to Thread-Local Data: You can take the address of a thread-local variable using the & operator and pass it to another thread. However, the address remains valid only while the thread that owns the memory is alive. Accessing this pointer after the owning thread terminates results in undefined behavior.
  • Compatibility: Before C11, compilers provided non-standard extensions, such as __thread (GCC/Clang) and __declspec(thread) (MSVC). While still widely supported, _Thread_local and the C23 thread_local keyword are the standard, portable methods for C applications.