Thread-Specific Data (TSD)

 

Chapter 31 · File 3 of 5
Thread-Specific Data (TSD)
Concepts, API, and Internal Implementation — Per-Thread Private Storage
Topics Covered:

TSD Concept pthread_key_create() pthread_key_delete() pthread_setspecific() pthread_getspecific() Destructor Function Internal Implementation Key Limits

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).

TSD: Each Thread Has Its Own Copy
Thread A
TSD buffer
for myfunc()
in Thread A
Thread B
TSD buffer
for myfunc()
in Thread B
Thread C
TSD buffer
for myfunc()
in Thread C
All three buffers correspond to the same TSD key, but each thread has its own isolated copy.

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:

1
Allocate per-thread storage once. The first time each thread calls the function, a storage block must be allocated for that thread. On subsequent calls by the same thread, the same block is reused.
2
Find the right storage block on each call. The function needs a way to get back the pointer to the storage block it allocated for this particular thread. It can’t use a static variable (shared by all threads) or a local variable (lost when function returns). The Pthreads API handles this.
3
Distinguish keys across different functions. Multiple library functions may all use TSD. Each needs its own “key” to identify its own storage, separate from the keys used by other functions.
4
Automatically clean up when a thread exits. The function doesn’t control when threads terminate. A destructor function is registered with the key so that when a thread exits, all its TSD buffers are automatically freed. Without this, memory would leak every time a thread terminates.

4. The Thread-Specific Data API

4.1 pthread_key_create() — Create a Key

#include <pthread.h>

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()

#include <pthread.h>

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) — Associates value (a pointer) with key for 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 for key in the calling thread. If this thread has never called pthread_setspecific() for this key, it returns NULL. 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.

Process-Wide pthread_keys[] 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.

Per-Thread tsd[] Arrays — Each Thread Has Its Own
Thread A — tsd[]
Index Value (pointer)
tsd[0] 0xA001 → bufA0
tsd[1] 0xA002 → bufA1
tsd[2] NULL
Thread B — tsd[]
Index Value (pointer)
tsd[0] 0xB001 → bufB0
tsd[1] 0xB002 → bufB1
tsd[2] NULL
tsd[1] for Thread A and tsd[1] for Thread B both correspond to key index 1,
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

1
Create the key once. Use 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.
2
Check if this thread has a buffer. Call pthread_getspecific(key). If it returns NULL, this is the first call from this thread.
3
Allocate a buffer for this thread (if first call). Call malloc() to allocate the buffer, then call pthread_setspecific(key, buf) to store the pointer for this thread.
4
Use the buffer. On all calls (first or subsequent), use the buffer pointer returned by pthread_getspecific(). Each thread uses its own buffer — no sharing, no race conditions.
5
Destructor frees the buffer. When a thread exits, the Pthreads library automatically calls the destructor registered in step 1, passing the thread’s buffer pointer. The destructor calls 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

Q1. What is Thread-Specific Data and what problem does it solve?Thread-Specific Data (TSD) is a Pthreads mechanism that provides each thread with its own private copy of a variable. It solves the problem of making existing non-thread-safe library functions thread-safe without changing their interface. By replacing a shared static variable with a per-thread TSD buffer, each thread gets its own isolated copy, eliminating data races without requiring callers to be modified.

Q2. What is a TSD key and what does 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.

Q3. What does 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().

Q4. What is the role of the destructor function in TSD?The destructor function is called automatically by the Pthreads library when a thread terminates, for each key where that thread has a non-NULL value. The destructor receives the thread’s stored pointer as its argument. Its job is typically to free the dynamically allocated TSD buffer. Without a destructor, every thread that calls the function and then terminates would leak memory, since the malloc’d buffer would never be freed.

Q5. Why must TSD key creation be done using 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.

Q6. What is the minimum number of TSD keys that a POSIX-compliant implementation must support?SUSv3 requires a minimum of 128 keys (_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.

Q7. How is TSD different from a thread-local variable declared with __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.

Leave a Reply

Your email address will not be published. Required fields are marked *