8.5. Thread-specific data

POSIX provides a mechanism that enables applications to maintain specified data on a per-Pthread basis. The mechanism is motivated by the need of some modules (that is, groups of related functions) to maintain selected data across function invocations (that is, to maintain static data in the C programming language). If such a module is being used by multiple Pthreads of a multi-threaded process, then the module may need to maintain such data separately for each calling Pthread, depending on the particular application.

Note

If a Pthread executes only one function during its life-cycle, it less likely needs thread-specific data. Because each Pthread has its own thread stack, all auto storage class variables in that function are allocated in the thread stack, which would be automatically deallocated when the Pthread terminates (see 8.3.4, “Thread stack” on page 292).


For example, consider a module consisting of the push, pop, and clear stack functions. Suppose that the module declares the stack and stack pointer as static data, instead of as function parameters. If the module is being used in a multi-threaded process, in which each Pthread needs its own stack and stack pointer, the module must have a mechanism for maintaining per-thread stacks and stack pointers.

In the POSIX model, per-thread data is maintained through a key/value mechanism, an approach designed for efficiency and ease of use. A key is an opaque object of type pthread_key_t. The value of a key is thread-specific, that is, each key that has been established for a multi-threaded process has a distinct value for each Pthread of the process. For this reason, the thread-specific data of a process is sometimes thought of as a matrix, with rows corresponding to keys and columns to Pthreads, although implementations need not work this way. A process can have up to PTHREAD_KEYS_MAX keys (or rows), where PTHREAD_KEYS_MAX is defined at least at 128 in the POSIX thread standard.[14]

[14] Currently, AIX 5L Version 5.2 supports a maximum of 450 keys per process. However, it can be increased in the future versions of AIX. Call the sysconf() routine with _SC_THREAD_KEYS_MAX to determine the maximum number of keys.

Keys are of opaque data type so that operating system implementations can have the freedom in setting them up to offer efficient access to thread-specific data. Instead of holding thread-specific values directly, keys may hold means of accessing thread-specific values. Conceptually, a key isolates a row of the thread-specific data matrix, and then the key uses the Pthread ID of the calling Pthread (the Pthread calling pthread_getspecific() or pthread_setspecific()) to isolate an entry in the row, thus obtaining the desired key value.

Typically, the value associated with a given key for a given Pthread is a pointer to memory dynamically allocated for the exclusive use of the given Pthread (for example, per-thread stack and stack pointer). The scenario for establishment and use of thread-specific data can be described as follows. A module that needs to maintain static data on a per-thread basis creates a new thread-specific data key as a part of its initialization routine. At initialization, all Pthreads of the process are assigned null values for the new key. Then, upon each Pthread’s first invocation of the module (which can be determined by checking for a null key value), the module dynamically allocates memory for the exclusive use of the calling Pthread, and stores a pointer to the memory as the calling Pthread’s value of the new key. Upon later invocations of the same module by the same Pthread, the module can access the Pthread’s data through the new key (that is, the Pthread’s value for the key). Other modules can independently create other thread-specific data keys for other per-thread data for their own use.

An application process could maintain thread-specific data in other ways. For example, it could use a hash function on a Pthread ID as a means of access to an area of thread-specific data. Then the application process would have to manage the use of sub-areas of the thread-specific data. The POSIX thread-specific data interfaces, on the other hand, provide Pthreads more direct access to sub-areas of their thread-specific data. Moreover, the sub-areas are independent; different parts of the application process can use different sub-areas without any need for process-wide cooperation.

8.5.1. Allocating thread-specific data

The pthread_key_create() function is used to allocate a new key. This key now becomes valid for all Pthreads in our process. When a key is created, the value it points to defaults to NULL. Later on, each Pthread may change its copy of the value as it wishes. The following code fragment shows how to use this function:

/* rc is used to contain return values of pthread functions. */
int rc;
/* define a variable to hold the key, once created. */
pthread_key_t list_key;
/*
 * cleanup_list() is a function that can clean up some data.
 * it is specific to our program, not to thread-specific data.
 */
extern void* cleanup_list(void*);
/* create the key, supplying a function that'll be invoked when it's deleted.*/
rc = pthread_key_create(&list_key, cleanup_list);

Please note the following:

  • After pthread_key_create() returns, the variable list_key points to the newly created key.

  • The function pointer, passed as a second parameter to pthread_key_create(), will be automatically invoked by the Pthread library when our Pthread exits, with a pointer to the key’s value as its parameter. We may supply a NULL pointer as the function pointer, and then no function will be invoked for key. Note that the function will be invoked once in each Pthread, even though we created this key only once in one Pthread.

  • If we created several keys, their associated destructor functions will be called in an arbitrary order, regardless of the order of keys creation.

  • If the pthread_key_create() function succeeds, it returns 0. Otherwise, it returns some error code.

  • There is a limit of PTHREAD_KEYS_MAX keys that may exist in our process at any given time. An attempt to create a key after PTHREAD_KEYS_MAX exits will cause a return value of EAGAIN from the pthread_key_create() function.

8.5.2. Accessing thread-specific data

After we have created a key, we may access its value using two Pthread functions: pthread_getspecific() and pthread_setspecific(). The first is used to get the value of a given key, and the second is used to set the data of a given key. A key’s value is simply a void pointer (void *), so we can store in it anything that we want. The following code fragment shows you how to use these functions, assuming that a_key is a properly initialized variable of type pthread_key_t, which contains a previously created key:

/* this variable will be used to store return codes of pthread functions. */
int rc;

/* define a variable into which we'll store some data. */
int* p_num = (int *)malloc(sizeof(int));
if (!p_num) {
    perror("malloc() failed.
");
    exit(1);
}
/* initialize our variable to some value. */
*p_num = 4;

/*
 * now let's store this value in our thread-specific data key.
 * note that we don't store p_num in our key.
 * we store the value that p_num points to.
 */
rc = pthread_setspecific(a_key, (void *)p_num);
....
....
/*
 * and somewhere later in our code, get the value of key a_key and print it.
 */
int* p_keyval = (int *)pthread_getspecific(a_key);

if (p_keyval != NULL) {
    printf("value of 'a_key' is: %d
", *p_keyval);
}

Note that if we set the value of the key in one Pthread, and try to get it in another Pthread, we will get a NULL, since this value is distinct for each Pthread.

There are two other cases where pthread_getspecific() might return NULL:

  • The key supplied as a parameter is invalid (for example, its key was not created).

  • The value of this key is NULL. This means it either was not initialized, or was set to NULL explicitly by a previous call to pthread_setspecific().

8.5.3. Deleting thread-specific data

The pthread_key_delete() function may be used to delete keys. It does not delete memory associated with this key or call the destructor function in C++ defined during the key’s creation. Thus, we still need to do memory cleanup on our own if we need to free this memory during run time. However, with global variables (and thus also thread-specific data), we usually do not need to free this memory until the thread terminates, in which case the pthread library will invoke our destructor functions anyway.

Using this function is simple. Assuming list_key is a pthread_key_t variable pointing to a properly created key, use this function like this:

int rc = pthread_key_delete(key);

The function will return 0 on success, or EINVAL if the supplied variable does not point to a valid thread-specific data key.

An example of using thread-specific data

The example program shown in Example 8-11 illustrates the use of thread-specific data routines. Each created Pthread maintains its own copy of the data structure tsd_data, which is declared globally.

Example 8-11. pthread_tsd.c
#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>

#define EXIT_CODE -1
/*
 * Structure used as the value for thread-specific data key.
 */
typedef struct tsd_data {
    pthread_t   thread_id;
    char        *mesg;
} tsd_data_var;
pthread_key_t tsd_data_key;           /* thread-specific data key */
pthread_once_t key_once = PTHREAD_ONCE_INIT;

/*
 * init_routine() is an one-time initialization routine used with
 * pthread_once().
*/
void init_routine(void)
{
    int rc;

    printf("Initializing key.
");
    if ((rc = pthread_key_create(&tsd_data_key, NULL)) != 0) {
        fprintf(stderr, "pthread_create() failed with rc = %d.
", rc);
        exit(EXIT_CODE);
    }
}

/*
 * tsd_routine() is a routine that uses pthread_once() to dynamically
 * create a thread-specific data key.
 */
void *tsd_routine(void *arg)
{
    tsd_data_var *value;
    int rc;

    if ((rc = pthread_once(&key_once, init_routine)) != 0) {
        fprintf(stderr, "pthread_once() failed with rc = %d.
", rc);
        exit(EXIT_CODE);
    }
    value = (tsd_data_var *)malloc(sizeof(tsd_data_var));
    if (value == NULL) {
        perror("Failed to allocate key value.");
        exit(EXIT_CODE);
    }
    if ((rc = pthread_setspecific(tsd_data_key, value)) != 0) {
        fprintf(stderr, "pthread_setspecific() failed with rc = %d.
", rc);
        exit(EXIT_CODE);
    }
    printf("%s set thread-specific data value %p
", arg, value);
    value->thread_id = pthread_self();
    value->mesg = (char *)arg;
    value = (tsd_data_var *)pthread_getspecific(tsd_data_key);
    printf("%s Starting...
", value->mesg);
    sleep(2);
    value = (tsd_data_var *)pthread_getspecific(tsd_data_key);
    printf("%s Done...
", value->mesg);
    return NULL;
}

int main(int argc, char *argv[])
{
    pthread_t thread1, thread2;
    int rc;

    if ((rc = pthread_create(&thread1, NULL, tsd_routine, "thread 1")) != 0) {
        fprintf(stderr, "pthread_create(#1) failed with rc = %d.
", rc);
        exit(EXIT_CODE);
    }
    if ((rc = pthread_create(&thread2, NULL, tsd_routine, "thread 2")) != 0) {
        fprintf(stderr, "pthread_create(#2) failed with rc = %d.
", rc);
        exit(EXIT_CODE);
    }
    pthread_exit (NULL);
}

The output of Example 8-11 on page 316 is:

Initializing key.
thread 1 set thread-specific data value 20209d58
thread 1 Starting...
thread 2 set thread-specific data value 20209d68
thread 2 Starting...
thread 1 Done...
thread 2 Done...

In Example 8-11 on page 316, the following two new Pthread routines are called: pthread_once() and pthread_self().

We have called the pthread_once() routine to dynamically create a Pthread specific data key. The pthread_once() routine calls an initialization routine executed by one Pthread a single time. This routine allows you to create your own initialization code that is guaranteed to be run only once, even if called simultaneously by multiple Pthreads or multiple times in the same Pthread.

For example, a mutex or a thread-specific data key must be created exactly once. Calling pthread_once() prevents the code that creates a mutex or thread-specific data from being called by multiple Pthreads. Without this routine, the execution must be serialized so that only one Pthread performs the initialization. Other Pthreads that reach the same point in the code are delayed until the first Pthread has finished the call to pthread_once().

This routine initializes the control record if it has not been initialized and then determines if the client one-time initialization routine has executed once. If it has not executed, this routine calls the initialization routine specified in init_routine().

If the client one-time initialization code has executed once, this routine returns. The pthread_once_t data structure is a record that allows client initialization operations to guarantee mutual exclusion of access to the initialization routine, and that each initialization routine is executed exactly once.

This variable must be initialized using the pthread_once_init macro, as follows:

pthread_once_t key_once = PTHREAD_ONCE_INIT;

Similarly, we have called the pthread_self() routine to get the unique identifier of the Pthread that is getting executed.

8.5.4. Thread-safe and reentrant functions

One very important point to take care of when building multi-threaded programs is the resource handling. To avoid getting in trouble, be sure to call only thread-safe and reentrant functions in multi-threaded applications. Reentrance and thread-safety are separate concepts: A function can be either reentrant, thread-safe, both, or neither.

ReentrantA reentrant function does not hold static data over successive calls, nor does it return a pointer to static data. All data is provided by the caller of the function. A reentrant function must not call non-reentrant functions.
Thread-safeA thread-safe function protects shared resources from concurrent access by locks. Thread-safety concerns only the implementation of a function and does not affect its external interface. The use of global data is thread-unsafe. It should be maintained per-Pthread or encapsulated so that its access can be serialized.

Reentrant and thread-safe libraries are useful in a wide range of parallel (and asynchronous) programming environments, not just within Pthreads. Thus, it is a good programming practice to always use and write reentrant and thread-safe functions.

Some of the standard C subroutines are non-reentrant, such as the ctime() and strtok() subroutines. The reentrant version of the subroutines typically have the name of the original subroutine with a suffix _r (underscore r).

When writing multi-threaded programs, the reentrant versions of subroutines should be used instead of the original version. For example, the following code fragment:

token[0] = strtok(string, separators);
i = 0;
do {
    i++;
    token[i] = strtok(NULL, separators);
} while (token[i] != NULL);

must be replaced by the following code fragment in the multi-threaded programming:

char *pointer;
...
token[0] = strtok_r(string, separators, &pointer);
i = 0;
do {
    i++;
    token[i] = strtok_r(NULL, separators, &pointer);
} while (token[i] != NULL);

Thread-unsafe libraries may be used by only one Pthread in a program. The uniqueness of the Pthread using the library must be ensured by the programmer; otherwise, the program will have unexpected behavior or may even crash.

In order to determine which sub routines provided by the AIX base operating system are reentrant or thread-safe, please refer to the “Threads Programming Guidelines” section in AIX 5L Version 5.2 General Programming Concepts: Writing and Debugging Programs.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset