strerror() Thread-Safe Using Thread-Specific Data — A Complete Walkthrough1. Background: The strerror() Function
strerror(int errnum) is a standard C library function that takes an error number (like EINVAL, EPERM) and returns a human-readable string describing the error (like "Invalid argument", "Operation not permitted").
Its signature returns a char * — a pointer to a string. In the classic (non-thread-safe) implementation, this pointer points to a static internal buffer. There is only one such buffer in the entire process, shared by all threads.
This means: if Thread A calls strerror(EINVAL) and Thread B calls strerror(EPERM), whichever writes second overwrites the buffer — and Thread A’s pointer now points to Thread B’s message. Both threads see the same string, which is wrong.
SUSv3 does not require strerror() to be thread-safe. This makes it an ideal case study for learning TSD — we need thread safety, but we cannot change the function’s interface (it must still return char * without any extra buffer argument).
2. The Non-Thread-Safe Implementation
Here is a simple, non-thread-safe implementation of strerror() similar to what the book shows. This uses a static buffer that all threads share:
/* ===== Listing 31-1: Non-thread-safe strerror() — THE PROBLEM ===== */
/* File: strerror.c */
#define _GNU_SOURCE /* Get _sys_errlist and _sys_nerr from <stdio.h> */
#include <stdio.h>
#include <string.h>
#define MAX_ERROR_LEN 256
/* PROBLEM: This is a STATIC variable — ONE copy for the ENTIRE PROCESS.
All threads share this single buffer. */
static char buf[MAX_ERROR_LEN];
char *strerror(int err)
{
if (err < 0 || err >= _sys_nerr || _sys_errlist[err] == NULL) {
snprintf(buf, MAX_ERROR_LEN, "Unknown error %d", err);
} else {
strncpy(buf, _sys_errlist[err], MAX_ERROR_LEN - 1);
buf[MAX_ERROR_LEN - 1] = '\0';
}
return buf; /* Returns pointer to SHARED static buffer */
}
static char buf[] exists at a fixed address in the BSS segment. Every call to strerror() from any thread writes into the same buffer and returns the same address. If Thread B writes after Thread A, Thread A’s saved pointer now points to Thread B’s error string.3. Demonstrating the Race Condition
The book shows a test program that calls strerror() from two threads, saves the returned pointer, and then prints both pointers after both threads have called the function. The key observation is that both pointers print the same string:
/* ===== Listing 31-2: Test program showing strerror() is not thread-safe ===== */
#include <stdio.h>
#include <string.h>
#include <pthread.h>
#include <errno.h>
#include <stdlib.h>
static void *threadFunc(void *arg)
{
char *str;
printf("Other thread about to call strerror()\n");
str = strerror(EPERM); /* "Operation not permitted" */
printf("Other thread: str (%p) = %s\n", str, str);
return NULL;
}
int main(int argc, char *argv[])
{
pthread_t t;
char *str;
int s;
str = strerror(EINVAL); /* "Invalid argument" — Thread A's call */
printf("Main thread has called strerror()\n");
s = pthread_create(&t, NULL, threadFunc, NULL);
if (s != 0) { perror("pthread_create"); exit(1); }
s = pthread_join(t, NULL);
if (s != 0) { perror("pthread_join"); exit(1); }
/* Both threads have called strerror(). What does main thread's str show? */
printf("Main thread: str (%p) = %s\n", str, str);
/* Both str pointers print THE SAME ADDRESS and THE SAME STRING ("EPERM") */
/* Thread B's write overwrote Thread A's result in the static buffer */
exit(EXIT_SUCCESS);
}
/* Sample output (non-thread-safe version):
Main thread has called strerror()
Other thread about to call strerror()
Other thread: str (0x804a7c0) = Operation not permitted
Main thread: str (0x804a7c0) = Operation not permitted
^^^^^^^^^^^
SAME ADDRESS — shared static buffer!
Main thread's "Invalid argument" was OVERWRITTEN by the other thread. */
| Time | Main Thread (EINVAL) | Other Thread (EPERM) | Static buf content |
|---|---|---|---|
| t=1 | Calls strerror(EINVAL) | “Invalid argument” | |
| t=2 | str = 0x804a7c0 ← static buf | “Invalid argument” | |
| t=3 | Calls strerror(EPERM) | “Operation not permitted” ← OVERWRITTEN! | |
| t=4 | Prints *str → sees “Operation not permitted” ❌ | Prints str → correct ✓ | “Operation not permitted” |
4. Thread-Safe strerror() Using TSD
Now we replace the single static buffer with a per-thread buffer using TSD. The function’s interface stays the same (char *strerror(int)) — callers don’t need to change.
Each thread gets its own buffer, allocated lazily (on the first call from that thread). The buffer persists across multiple calls from the same thread. When the thread exits, the destructor frees the buffer.
/* ===== Listing 31-3: Thread-safe strerror() using TSD ===== */
/* File: strerror_tsd.c */
#define _GNU_SOURCE
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <pthread.h>
#define MAX_ERROR_LEN 256
/* ❶ Control variable for one-time key creation */
static pthread_once_t once = PTHREAD_ONCE_INIT;
/* ❷ The TSD key — shared by all threads, but points to per-thread data */
static pthread_key_t strerrorKey;
/* ❸ Destructor: called automatically when a thread exits */
static void destructor(void *buf)
{
free(buf); /* Free this thread's buffer */
/* Note: Pthreads only calls this if buf != NULL */
}
/* ❹ Key creation function — called exactly once by pthread_once() */
static void createKey(void)
{
int s;
/* Create the key and register the destructor */
s = pthread_key_create(&strerrorKey, destructor);
if (s != 0) {
fprintf(stderr, "pthread_key_create failed: %d\n", s);
exit(EXIT_FAILURE);
}
}
/* ❺ Thread-safe strerror() — same interface as before */
char *strerror(int err)
{
int s;
char *buf;
/* Step 1: Ensure key is created exactly once (first call from any thread) */
s = pthread_once(&once, createKey);
if (s != 0) {
fprintf(stderr, "pthread_once failed: %d\n", s);
exit(EXIT_FAILURE);
}
/* Step 2: Get this thread's buffer (NULL if first call from this thread) */
buf = pthread_getspecific(strerrorKey);
if (buf == NULL) {
/* Step 3: First call from this thread — allocate a buffer */
buf = malloc(MAX_ERROR_LEN);
if (buf == NULL) {
fprintf(stderr, "malloc failed\n");
exit(EXIT_FAILURE);
}
/* Step 4: Save pointer so we can retrieve it on next call from this thread */
s = pthread_setspecific(strerrorKey, buf);
if (s != 0) {
fprintf(stderr, "pthread_setspecific failed: %d\n", s);
exit(EXIT_FAILURE);
}
}
/* If buf != NULL: this thread already has a buffer from a previous call */
/* Step 5: Write error string into THIS THREAD'S OWN buffer */
if (err < 0 || err >= _sys_nerr || _sys_errlist[err] == NULL) {
snprintf(buf, MAX_ERROR_LEN, "Unknown error %d", err);
} else {
strncpy(buf, _sys_errlist[err], MAX_ERROR_LEN - 1);
buf[MAX_ERROR_LEN - 1] = '\0';
}
return buf; /* Returns pointer to THIS THREAD's private buffer */
}
/* ===== Test the thread-safe version ===== */
static void *threadFunc(void *arg)
{
char *str;
printf("Other thread about to call strerror()\n");
str = strerror(EPERM);
printf("Other thread: str (%p) = %s\n", str, str);
return NULL;
/* When this thread exits, destructor(buf) is called automatically — free()! */
}
int main(void)
{
pthread_t t;
char *str;
int s;
str = strerror(EINVAL);
printf("Main thread has called strerror()\n");
s = pthread_create(&t, NULL, threadFunc, NULL);
if (s != 0) { perror("pthread_create"); exit(1); }
s = pthread_join(t, NULL);
if (s != 0) { perror("pthread_join"); exit(1); }
printf("Main thread: str (%p) = %s\n", str, str);
return 0;
}
/* Output (thread-safe version):
Main thread has called strerror()
Other thread about to call strerror()
Other thread: str (0x804b158) = Operation not permitted
Main thread: str (0x804b008) = Invalid argument
^^^^^^^^^^^ ^^^^^^^^^^^^^^^^
DIFFERENT ADDRESS — each thread has its own buffer!
Main thread's result is PRESERVED. */
5. Step-by-Step Walkthrough of the TSD Flow
Let’s trace exactly what happens when two threads call the TSD-based strerror():
strerror(EINVAL) for the first time.pthread_once() sees once == PTHREAD_ONCE_INIT, so it calls createKey(). pthread_key_create() allocates a slot (say, index 1) in the global keys array, stores the destructor, and returns 1 as the key. once is marked “done”.pthread_getspecific(strerrorKey).Main thread’s
tsd[1] is NULL (thread was just created). Returns NULL.malloc(256) returns, say, address 0x804b008. pthread_setspecific(strerrorKey, 0x804b008) stores this in main thread’s tsd[1]."Invalid argument" into its buffer at 0x804b008.Returns
0x804b008. Main thread saves this in str.strerror(EPERM).pthread_once() sees once is already “done” — skips createKey(). pthread_getspecific(strerrorKey) checks THIS THREAD’s tsd[1] — it is NULL. Allocates a NEW buffer at 0x804b158. Stores it in other thread’s tsd[1]."Operation not permitted" into its buffer at 0x804b158.This is a DIFFERENT address from main thread’s buffer. Main thread’s buffer at 0x804b008 is untouched.
Pthreads sees
tsd[1] is non-NULL for this thread. Calls destructor(0x804b158) automatically. free(0x804b158) — no memory leak.6. Building on TSD: A Thread-Safe Counter with Per-Thread Stats
/* ===== EXAMPLE: Thread-safe function maintaining per-thread statistics ===== */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <pthread.h>
/* Per-thread statistics structure */
typedef struct {
long call_count; /* How many times this thread called the function */
long total_value; /* Sum of all values processed by this thread */
char last_op[64]; /* Description of last operation */
} ThreadStats;
static pthread_key_t stats_key;
static pthread_once_t once = PTHREAD_ONCE_INIT;
static void free_stats(void *stats) {
printf("Thread exiting — call_count=%ld, total=%ld\n",
((ThreadStats *)stats)->call_count,
((ThreadStats *)stats)->total_value);
free(stats);
}
static void create_stats_key(void) {
pthread_key_create(&stats_key, free_stats);
}
/* Get (or create) this thread's stats — typical TSD pattern */
static ThreadStats *get_my_stats(void)
{
ThreadStats *ts;
pthread_once(&once, create_stats_key); /* One-time key creation */
ts = pthread_getspecific(stats_key); /* Get this thread's pointer */
if (ts == NULL) { /* First call from this thread */
ts = calloc(1, sizeof(ThreadStats)); /* Allocate per-thread struct */
pthread_setspecific(stats_key, ts); /* Store for future calls */
}
return ts;
}
/* Library function — thread-safe, no interface change, tracks per-thread stats */
long compute_and_track(long value)
{
ThreadStats *ts = get_my_stats();
ts->call_count++;
ts->total_value += value;
snprintf(ts->last_op, sizeof(ts->last_op), "processed value %ld", value);
return value * value; /* some computation */
}
static void *worker(void *arg)
{
long id = (long)arg;
int i;
for (i = 0; i < 3; i++) {
long result = compute_and_track(id * 10 + i);
printf("Thread %ld: compute_and_track(%ld) = %ld\n", id, id*10+i, result);
}
/* When thread exits, free_stats() is called automatically, printing stats */
return NULL;
}
int main(void)
{
pthread_t t1, t2;
pthread_create(&t1, NULL, worker, (void *)1L);
pthread_create(&t2, NULL, worker, (void *)2L);
pthread_join(t1, NULL);
pthread_join(t2, NULL);
return 0;
}
Interview Questions
strerror() function not thread-safe?The standard implementation of strerror() returns a pointer to a static internal buffer. There is only one such buffer in the entire process, shared by all threads. If two threads call strerror() simultaneously or in quick succession, the second call overwrites the buffer, corrupting the result that the first thread was still using. The pointer returned by the first call now points to the second thread’s error message.
strerror() fix the thread safety problem?Instead of a single shared static buffer, the TSD version gives each thread its own dynamically allocated buffer. The first time a thread calls the function, it allocates a buffer with malloc() and stores the pointer using pthread_setspecific(). On subsequent calls from the same thread, it retrieves the same buffer with pthread_getspecific(). Each thread writes its error string into its own buffer, so threads can never overwrite each other’s results.
malloc(). If no destructor is registered with pthread_key_create(), the buffer is never freed when the thread exits. In a server that creates and destroys many threads over time, this accumulates into a significant memory leak. Registering a destructor that calls free() ensures automatic cleanup at thread termination.
pthread_once() used for key creation rather than just calling pthread_key_create() directly?pthread_key_create() must be called exactly once because the key is a process-wide resource. If two threads simultaneously call strerror() for the first time without synchronization, both might call pthread_key_create(), creating two different keys. Each thread would store its buffer under a different key, and lookups would return NULL (wrong key), causing each thread to allocate a new buffer on every call. pthread_once() prevents this by ensuring key creation runs exactly once.
strerror() when the test program is run?With the non-thread-safe version, both threads print the SAME memory address for their str pointer, and both see the same error message (whichever thread wrote last). With the thread-safe TSD version, each thread’s str pointer points to a DIFFERENT address — their own private buffers — and each thread correctly sees its own error message: main thread sees “Invalid argument” (EINVAL) and other thread sees “Operation not permitted” (EPERM).
pthread_setspecific() be something other than a pointer to a heap buffer?Yes. The value is typed as void * but can technically hold any value that fits in a pointer. For example, you could store an integer by casting: pthread_setspecific(key, (void *)(uintptr_t)42). In this case, you would register no destructor (pass NULL to pthread_key_create()) since there is nothing to free. However, this is an unusual pattern — most common usage stores a pointer to a malloc()-allocated structure.
