4.3 Thread-Specific Data

Unlike processes, all threads in a single program share the same address space. This means that if one thread modifies a location in memory (for instance, a global variable), the change is visible to all other threads. This allows multiple threads to operate on the same data without the use interprocess communication mechanisms (which are described in Chapter 5).

Each thread has its own call stack, however. This allows each thread to execute different code and to call and return from subroutines in the usual way. As in a single-threaded program, each invocation of a subroutine in each thread has its own set of local variables, which are stored on the stack for that thread.

Sometimes, however, it is desirable to duplicate a certain variable so that each thread has a separate copy. GNU/Linux supports this by providing each thread with a thread-specific data area. The variables stored in this area are duplicated for each thread, and each thread may modify its copy of a variable without affecting other threads. Because all threads share the same memory space, thread-specific data may not be accessed using normal variable references. GNU/Linux provides special functions for setting and retrieving values from the thread-specific data area.

You may create as many thread-specific data items as you want, each of type void*. Each item is referenced by a key. To create a new key, and thus a new data item for each thread, use pthread_key_create. The first argument is a pointer to a pthread_key_t variable. That key value can be used by each thread to access its own copy of the corresponding data item. The second argument to pthread_key_t is a cleanup function. If you pass a function pointer here, GNU/Linux automatically calls that function when each thread exits, passing the thread-specific value corresponding to that key. This is particularly handy because the cleanup function is called even if the thread is canceled at some arbitrary point in its execution. If the thread-specific value is null, the thread cleanup function is not called. If you don't need a cleanup function, you may pass null instead of a function pointer.

After you've created a key, each thread can set its thread-specific value corresponding to that key by calling pthread_setspecific. The first argument is the key, and the second is the void* thread-specific value to store. To retrieve a thread-specific data item, call pthread_getspecific, passing the key as its argument.

Suppose, for instance, that your application divides a task among multiple threads. For audit purposes, each thread is to have a separate log file, in which progress messages for that thread's tasks are recorded. The thread-specific data area is a convenient place to store the file pointer for the log file for each individual thread.

Listing 4.7 shows how you might implement this. The main function in this sample program creates a key to store the thread-specific file pointer and then stores it in thread_log_key. Because this is a global variable, it is shared by all threads. When each thread starts executing its thread function, it opens a log file and stores the file pointer under that key. Later, any of these threads may call write_to_thread_log to write a message to the thread-specific log file. That function retrieves the file pointer for the thread's log file from thread-specific data and writes the message.

Listing 4.7 (tsd.c) Per-Thread Log Files Implemented with Thread-Specific Data
#include <malloc.h> 
#include <pthread.h> 
#include <stdio.h> 
 
/* The key used to associate a log file pointer with each thread.  */ 
static pthread_key_t  thread_log_key; 
 
/* Write MESSAGE to the log file for the current thread.  */ 
 
void write_to_thread_log (const char* message) 
{
   FILE* thread_log  =  (FILE*) pthread_getspecific (thread_log_key); 
   fprintf (thread_log,  "%s\n", message); 
} 
 
/* Close the log file pointer THREAD_LOG.  */ 
 
void close_thread_log (void*  thread_log) 
{
   fclose ((FILE*) thread_log); 
} 
 
void* thread_function (void* args) 
{
   char thread_log_filename[20]; 
   FILE* thread_log; 
 
   /* Generate the filename for this thread's log file.  */ 
   sprintf (thread_log_filename, "thread%d.log", (int) pthread_self ()); 
   /* Open the log file.  */ 
   thread_log = fopen (thread_log_filename, "w"); 
   /* Store the file pointer in thread-specific data under thread_log_key.  */ 
   pthread_setspecific (thread_log_key, thread_log); 
 
   write_to_thread_log ("Thread starting."); 
   /* Do work here...  */ 
 
   return NULL; 
} 
 
int main () 
{
   int i; 
   pthread_t threads[5]; 
 
   /* Create a key to associate thread log file pointers in 
      thread-specific data. Use close_thread_log to clean up the file 
      pointers.  */ 
   pthread_key_create (&thread_log_key, close_thread_log); 
   /* Create threads to do the work.  */ 
   for (i = 0; i < 5; ++i) 
      pthread_create (&(threads[i]), NULL, thread_function, NULL); 
   /* Wait for all threads to finish.  */ 
   for (i = 0; i < 5; ++i) 
      pthread_join (threads[i], NULL); 
   return 0; 
} 

Observe that thread_function does not need to close the log file. That's because when the log file key was created, close_thread_log was specified as the cleanup function for that key. Whenever a thread exits, GNU/Linux calls that function, passing the thread-specific value for the thread log key. This function takes care of closing the log file.

4.3.9 Cleanup Handlers

The cleanup functions for thread-specific data keys can be very handy for ensuring that resources are not leaked when a thread exits or is canceled. Sometimes, though, it's useful to be able to specify cleanup functions without creating a new thread-specific data item that's duplicated for each thread. GNU/Linux provides cleanup handlers for this purpose.

A cleanup handler is simply a function that should be called when a thread exits. The handler takes a single void* parameter, and its argument value is provided when the handler is registered—this makes it easy to use the same handler function to deallocate multiple resource instances.

A cleanup handler is a temporary measure, used to deallocate a resource only if the thread exits or is canceled instead of finishing execution of a particular region of code. Under normal circumstances, when the thread does not exit and is not canceled, the resource should be deallocated explicitly and the cleanup handler should be removed.

To register a cleanup handler, call pthread_cleanup_push, passing a pointer to the cleanup function and the value of its void* argument. The call to pthread_cleanup_push must be balanced by a corresponding call to pthread_cleanup_pop, which unregisters the cleanup handler. As a convenience, pthread_cleanup_pop takes an int flag argument; if the flag is nonzero, the cleanup action is actually performed as it is unregistered.

The program fragment in Listing 4.8 shows how you might use a cleanup handler to make sure that a dynamically allocated buffer is cleaned up if the thread terminates.

Listing 4.8 (cleanup.c) Program Fragment Demonstrating a Thread Cleanup Handler
#include  <malloc.h> 
#include  <pthread.h> 
 
/* Allocate  a  temporary  buffer.  */ 
 
void* allocate_buffer (size_t size) 
{
  return malloc (size); 
} 
 
/* Deallocate a temporary buffer. */ 
 
void deallocate_buffer (void* buffer) 
{
  free (buffer); 
} 
 
void do_some_work () 
{
  /* Allocate a temporary buffer. */ 
  void* temp_buffer = allocate_buffer (1024); 
  /* Register a cleanup handler for this buffer, to deallocate it in 
     case the thread exits or is cancelled.  */ 
  pthread_cleanup_push (deallocate_buffer, temp_buffer); 
 
  /* Do some work here that might call pthread_exit or might be 
     cancelled...  */ 
 
  /* Unregister the cleanup handler. Because we pass a nonzero value, 
     this actually performs the cleanup by calling 
     deallocate_buffer.  */ 
  pthread_cleanup_pop (1); 
} 

Because the argument to pthread_cleanup_pop is nonzero in this case, the cleanup function deallocate_buffer is called automatically here and does not need to be called explicitly. In this simple case, we could have used the standard library function free directly as our cleanup handler function instead of deallocate_buffer.

4.3.10 Thread Cleanup in C++

C++ programmers are accustomed to getting cleanup "for free" by wrapping cleanup actions in object destructors. When the objects go out of scope, either because a block is executed to completion or because an exception is thrown, C++ makes sure that destructors are called for those automatic variables that have them. This provides a handy mechanism to make sure that cleanup code is called no matter how the block is exited.

If a thread calls pthread_exit, though, C++ doesn't guarantee that destructors are called for all automatic variables on the thread's stack. A clever way to recover this functionality is to invoke pthread_exit at the top level of the thread function by throwing a special exception.

The program in Listing 4.9 demonstrates this. Using this technique, a function indicates its intention to exit the thread by throwing a ThreadExitException instead of calling pthread_exit directly. Because the exception is caught in the top-level thread function, all local variables on the thread's stack will be destroyed properly as the exception percolates up.

Listing 4.9 (cxx-exit.cpp) Implementing Safe Thread Exit with C++ Exceptions
#include  <pthread.h> 
 
class  ThreadExitException 
{
public: 
  /* Create  an  exception-signaling  thread  exit  with  RETURN_VALUE.  */ 
  ThreadExitException (void*  return_value) 
    : thread_return_value_  (return_value) 
{
} 
 
/*  Actually  exit  the  thread,  using  the  return  value  provided  in  the 
  constructor.  */ 
void*  DoThreadExit  () 
{
  pthread_exit  (thread_return_value_); 
} 
 
private: 
  /*  The  return  value  that  will  be  used  when  exiting  the  thread.   */ 
  void*  thread_return_value_; 
}; 
 
void  do_some_work  () 
{
  while  (1)  {
    /*  Do  some  useful  things  here...  */ 
 
    if  (should_exit_thread_immediately  ()) 
      throw  ThreadExitException  (/*  thread's  return  value  =  */ NULL); 
  } 
} 
 
void*  thread_function  (void*) 
{
  try  {
    do_some_work  (); 
  } 
  catch  (ThreadExitException  ex)  {
    /*   Some  function  indicated  that  we  should  exit  the  thread.  */ 
    ex.DoThreadExit  (); 
  } 
  return  NULL; 
}