In C, many standard library functions rely on static variables to keep track of state between calls. In Chapter 3, we wrote a function called readline (Figure 3.18). To make it fast, it read 4,096 bytes at a time into a static buffer and then returned one line at a time to the user.

The Conflict: If Thread A calls readline, the static buffer fills with Thread A’s data. If Thread B then calls readline (even on a different socket), it sees the same static buffer. Thread B might overwrite Thread A’s data, or read the leftovers of Thread A’s data.

We need a way to allow a function to keep “state,” but have that state be private to the thread calling it.

The Solution: Thread-Specific Data

Thread-Specific Data allows us to create a global “index” (like a key), but when different threads access that key, they each see their own private memory location.

Stevens explains this using a Key-Value model (Figures 26.7 and 26.8):

  1. The Key (Global Index): This is a small integer (e.g., Key 1) that everyone agrees on. It’s like a locker number in a gym. “Locker #1” exists for everyone.
  2. The Value (Thread-Private Pointer): When Thread A opens “Locker #1,” it sees its own gym bag. When Thread B opens “Locker #1,” it sees a completely different gym bag.

The 4 Main Functions

The book introduces four functions to manage this machinery that all run inside the spawned thread:

  1. pthread_key_create: “Install the lockers.”
    • You call this once (usually at program startup).
    • It gives you a generic Key ID (e.g., rl_key) to use later.
    • Crucial Feature: You can provide a Destructor function. When a thread dies, if it put anything in this locker, the Destructor is called to clean it up (free memory).
  2. pthread_once: “Do this only one time.”. Since pthread_key_create should only be called once, but we might have many threads starting at random times, we use this to ensure the key creation happens exactly once, no matter who gets there first.
  3. pthread_getspecific: “Open the locker.”
    • Call this with the Key ID to get your private pointer.
    • Returns NULL if you haven’t put anything there yet.
  4. pthread_setspecific: “Put something in the locker.” You malloc a structure and store the pointer here.
The readline Example (Figure 26.11)

The text reimagines the buggy readline function to be thread-safe using TSD.

#include    "unpthread.h"
 
static pthread_key_t rl_key;
static pthread_once_t rl_once = PTHREAD_ONCE_INIT;
 
static void
readline_destructor(void *ptr)
{
    free(ptr);
}
 
static void
readline_once(void)
{
    Pthread_key_create(&rl_key, readline_destructor);
}
 
typedef struct {
    int     rl_cnt;             /* initialize to 0 */
    char    *rl_bufptr;         /* initialize to rl_buf */
    char    rl_buf[MAXLINE];
} Rline;
 
static ssize_t
my_read(Rline *tsd, int fd, char *ptr)
{
    if (tsd->rl_cnt <= 0) {
        again:
        if ( (tsd->rl_cnt = read(fd, tsd->rl_buf, MAXLINE)) < 0) {
            if (errno == EINTR)
                goto again;
            return (-1);
        } else if (tsd->rl_cnt == 0)
            return (0);
        tsd->rl_bufptr = tsd->rl_buf;
    }
 
    tsd->rl_cnt--;
    *ptr = *tsd->rl_bufptr++;
    return (1);
}
 
ssize_t
readline(int fd, void *vptr, size_t maxlen)
{
    size_t  n, rc;
    char    c, *ptr;
    Rline   *tsd;
 
    Pthread_once(&rl_once, readline_once);
    if ( (tsd = pthread_getspecific(rl_key)) == NULL) {
        tsd = Calloc(1, sizeof(Rline)); /* init to 0 */
        Pthread_setspecific(rl_key, tsd);
    }
 
    ptr = vptr;
    for (n = 1; n < maxlen; n++) {
        if ( (rc = my_read(tsd, fd, &c)) == 1) {
            *ptr++ = c;
            if (c == '\n')
                break;
        } else if (rc == 0) {
            *ptr = 0;
            return (n - 1);     /* EOF, n - 1 bytes read */
        } else
            return (-1);        /* error, errno set by read() */
    }
 
    *ptr = 0;
    return (n);
}

The Flow:

  1. Initialization: The first time any thread calls readline, pthread_once creates a key named rl_key.
  2. Check: Every time readline is called, the thread checks: “Do I already have a buffer associated with rl_key?” (pthread_getspecific)
  3. Allocate (if needed):
    • If No (returns NULL): The thread calls malloc to allocate its own private Rline structure (containing the buffer). It saves this pointer using pthread_setspecific.
    • If Yes: It proceeds using the pointer it retrieved.
  4. Cleanup: When the thread terminates, the Destructor function (registered in step 1) automatically runs and frees the memory allocated in step 3.

Note

The function prototype is:

int pthread_once(pthread_once_t *onectrl, void (*init_routine)(void));

Since pthread_once is called inside a function that might be run by 100 threads, the system needs a place to store the answer to the question: “Did we do this already?”. rl_once is a special variable (usually just an integer/flag) that sits in global memory. It must be initialized to PTHREAD_ONCE_INIT (which usually equals 0).