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_localand the C23thread_localkeyword are the standard, portable methods for C applications.