1. What is Thread-Specific Data (TSD)?
Thread-Specific Data (TSD) is a mechanism that allows a function to maintain a separate copy of a variable for each thread that calls it. Each thread has its own private “slot” of data associated with a given key. Threads cannot see each other’s TSD.
TSD is designed to solve a specific problem: you have an existing library function that uses a static variable internally (making it non-thread-safe), but you cannot change the function’s interface — there are too many callers to update. TSD allows you to replace the single shared static variable with a per-thread copy, making the function thread-safe without any change to its API.
TSD data is persistent: a thread’s TSD value for a given key survives across multiple calls to the function. It only disappears when the thread terminates (at which point a destructor function can clean it up).
for myfunc()
in Thread A
for myfunc()
in Thread B
for myfunc()
in Thread C
2. When to Use TSD (vs Other Approaches)
| Approach | Interface Change? | Performance | Best For |
|---|---|---|---|
| Reentrant (caller provides buffer) | Yes — new function signature | Best | New functions, caller can be updated |
| Thread-Specific Data (TSD) | No — same interface | Good | Existing functions that cannot change their API |
| Thread-Local Storage (__thread) | No — same interface | Best among per-thread storage | Simpler cases; when compiler/kernel support available |
| Global mutex (serialize) | No | Worst (no parallelism) | Simple functions, low contention |
3. Four Challenges TSD Must Solve
Before looking at the API, it helps to understand the four specific problems that TSD must solve for a library function:
4. The Thread-Specific Data API
4.1 pthread_key_create() — Create a Key
int pthread_key_create(pthread_key_t *key, void (*destructor)(void *));
Returns: 0 on success, positive error number on error
This function creates a new TSD key and stores it in *key. The key is a process-wide identifier (an index into an internal array) that all threads can use to store and retrieve their own private data.
The destructor argument is a pointer to a function that will be called automatically when a thread that has non-NULL data for this key terminates. The destructor receives the thread’s data pointer as its argument and is responsible for freeing it. If no cleanup is needed, pass NULL.
Because the key is used by all threads, key should point to a global or static variable. Key creation is typically done once using pthread_once().
/* Destructor function — called automatically when thread exits */
static void free_tsd_buffer(void *buf)
{
free(buf); /* buf is the value that was set with pthread_setspecific() */
/* buf will not be NULL here — Pthreads only calls destructor if value != NULL */
}
/* One-time key creation */
static pthread_key_t my_key;
static pthread_once_t once = PTHREAD_ONCE_INIT;
static void create_key(void)
{
int s = pthread_key_create(&my_key, free_tsd_buffer);
if (s != 0) {
fprintf(stderr, "pthread_key_create failed: %d\n", s);
exit(1);
}
}
4.2 pthread_setspecific() and pthread_getspecific()
int pthread_setspecific(pthread_key_t key, const void *value);
Returns: 0 on success, positive error number on error
void *pthread_getspecific(pthread_key_t key);
Returns: pointer previously set for this key in this thread,
or NULL if no value has been set
These two functions form the core of the TSD API:
pthread_setspecific(key, value)— Associatesvalue(a pointer) withkeyfor the calling thread. Only the calling thread’s association is modified; other threads’ associations for the same key are unaffected.pthread_getspecific(key)— Returns the pointer that was previously set forkeyin the calling thread. If this thread has never calledpthread_setspecific()for this key, it returnsNULL. This NULL return is how a function detects “first call from this thread.”
5. How TSD Is Implemented Internally (NPTL)
Understanding the internal implementation helps explain the API design. A typical implementation (like NPTL, the Linux Pthreads implementation) uses two data structures:
5.1 The Global Keys Array (pthread_keys[])
There is one process-wide array called pthread_keys[]. Each element represents one TSD key and contains:
- An “in use” flag — whether this slot has been allocated by
pthread_key_create() - A destructor pointer — the function to call when a thread with this key’s data exits
The value returned by pthread_key_create() is simply an index into this array.
| Index | in_use flag | destructor pointer |
| pthread_keys[0] | 1 (in use) | free_buffer_A() |
| pthread_keys[1] | 1 (in use) | free_buffer_B() |
| pthread_keys[2] | 0 (free) | NULL |
| … | … | … |
5.2 Per-Thread TSD Pointer Arrays (tsd[])
Every thread also has its own private array called tsd[]. Each element corresponds to one key index from pthread_keys[]. It stores the pointer that was set by pthread_setspecific() for that key, in that thread.
When a thread is first created, all elements of its tsd[] array are initialized to NULL. This is why pthread_getspecific() returns NULL the first time a new thread calls it.
| Index | Value (pointer) |
| tsd[0] | 0xA001 → bufA0 |
| tsd[1] | 0xA002 → bufA1 |
| tsd[2] | NULL |
| Index | Value (pointer) |
| tsd[0] | 0xB001 → bufB0 |
| tsd[1] | 0xB002 → bufB1 |
| tsd[2] | NULL |
but they point to different buffers — Thread A’s buffer and Thread B’s buffer.
6. General Steps for Using TSD in a Library Function
pthread_once() to ensure pthread_key_create() is called exactly once, the first time any thread calls the function. This creates the key and registers the destructor.pthread_getspecific(key). If it returns NULL, this is the first call from this thread.malloc() to allocate the buffer, then call pthread_setspecific(key, buf) to store the pointer for this thread.pthread_getspecific(). Each thread uses its own buffer — no sharing, no race conditions.free().7. Limits on TSD Keys
Because the key is an index into a fixed-size array, implementations impose a limit on how many keys can exist simultaneously. SUSv3 requires implementations to support at least 128 keys (_POSIX_THREAD_KEYS_MAX). Linux supports up to 1024 keys.
In practice, 128 keys is more than enough. Each well-written library function should use only one or a small number of keys. If a function needs multiple per-thread values, they should be packed into a single structure with one associated key.
/* Good practice: pack multiple per-thread values into one struct, use ONE key */
typedef struct {
char error_buf[256]; /* Per-thread error string */
int last_error_code; /* Per-thread error code */
char temp_buffer[1024]; /* General-purpose per-thread scratch space */
} ThreadData;
/* One key for all per-thread data — conserves keys */
static pthread_key_t thread_data_key;
static pthread_once_t once = PTHREAD_ONCE_INIT;
static void free_thread_data(void *data) { free(data); }
static void create_thread_data_key(void) {
pthread_key_create(&thread_data_key, free_thread_data);
}
static ThreadData *get_thread_data(void) {
ThreadData *td;
pthread_once(&once, create_thread_data_key);
td = pthread_getspecific(thread_data_key);
if (td == NULL) {
td = calloc(1, sizeof(ThreadData)); /* allocate on first call */
pthread_setspecific(thread_data_key, td);
}
return td;
}
/* Usage in library functions */
void my_lib_function_A(int err) {
ThreadData *td = get_thread_data();
strerror_r(err, td->error_buf, sizeof(td->error_buf));
td->last_error_code = err;
}
void my_lib_function_B(const char *src) {
ThreadData *td = get_thread_data();
strncpy(td->temp_buffer, src, sizeof(td->temp_buffer) - 1);
/* Use td->temp_buffer safely — it's per-thread */
}
Interview Questions
pthread_key_create() actually create?A TSD key is an index into a global array of key information (the pthread_keys[] array). pthread_key_create() allocates one slot in this array, stores the destructor pointer there, and returns the index as the key. The key identifies a particular “slot” in every thread’s per-thread TSD pointer array. Creating a key does NOT allocate any per-thread storage — that happens separately, the first time each thread calls the function.
pthread_getspecific() return on the first call from a new thread?It returns NULL. When a thread is created, all elements of its per-thread TSD array are initialized to NULL. This is the mechanism by which a library function detects that this thread is calling it for the first time — if pthread_getspecific() returns NULL, the function knows it must allocate a new buffer for this thread and store it using pthread_setspecific().
pthread_once()?The key must be created exactly once because it is a process-wide resource — all threads share the same key. If two threads simultaneously called pthread_key_create() without synchronization, two different keys would be created. One thread would use key 0 to store its buffer, and another would use key 1, meaning threads would look up data at different keys and always get NULL. pthread_once() guarantees the key creation function runs exactly once.
_POSIX_THREAD_KEYS_MAX). On Linux, the actual limit is 1024 keys. In practice, well-designed library functions should minimize key usage — ideally one key per library, with multiple per-thread values packed into a single structure pointed to by that key.
__thread?Both provide per-thread storage, but they differ in complexity and usage. TSD requires explicit API calls (pthread_key_create, pthread_setspecific, pthread_getspecific) and manual memory management via a destructor. __thread is a compiler extension that makes the declaration itself per-thread — no API calls needed, no manual allocation. __thread is much simpler but requires compiler and kernel support and cannot be used for dynamically-sized per-thread data. TSD is more portable and flexible.
