Thread-Specific Data
Sometimes we need to be able to maintain data that is specific to a given thread but is referenced in a section of code that will be executed by multiple threads. Data of this nature, stored in a memory block private to the thread, is usually termed thread-specific data, or TSD. This data is referenced using a thread-specific pointer and the associated key. The keys for TSD are global to all the threads within the process. To make use of TSD, a thread must allocate and bind (associate) the key with the data. The library call pthread_key_create (Table 11.29) is used to allocate a new key. Only one thread should issue the create call for a TSD item.
Table 11.29. The pthread_key_create Library Function.
Include File(s) |
Manual Section |
3 |
||
Summary |
int pthread_key_create( pthread_key_t *key, void(*destr_function) (void *) ); |
|||
Return |
Success |
Failure |
Sets errno |
|
-1 |
As its first argument, the pthread_key_create library call is passed a reference, *key , to a pthread_key_t data type. The pthread_key_t data type is typedef ed as an unsigned int . If the call to pthread_key_create is successful, the *key argument references the newly allocated key. There is a system limit on the number of keys per thread. The limit is designated by the defined constant PTHREAD_KEYS_MAX.
The second argument for pthread_key_create is a reference to a destructor function. If this argument is non-NULL, the referenced function is called and passed the associated TSD when the thread exits (i.e., calls pthread_exit ) or is cancelled. If TSD is allocated within the destructor function, the system attempts to repeat destructor calls until all keys are NULL. As might be expected, this behavior could lead to some interesting recursive situations. Some thread implementations , such as LinuxThreads, limit the number of calls to resolve the removal of TSD; some do not. In LinuxThreads these calls are limited by the defined constant PTHREAD_DESTRUCTOR_ITERATIONS. If the call to pthread_key_create is successful, it returns a value of 0; otherwise , it returns the value EAGAIN (11) if the limit for the number of keys has been exceeded or ENOMEM (12) if insufficient memory is available to allocate the key.
The pthread_key_delete library function (Table 11.30) is used to remove the storage associated with a specific key (versus the data associated with the key).
Table 11.30. The pthread_key_delete Library Function.
Include File(s) |
Manual Section |
3 |
||
Summary |
int pthread_key_delete(pthread_key_t key); |
|||
Return |
Success |
Failure |
Sets errno |
|
-1 |
A valid TSD key is pthread_key_delete 's only argument. If the key value is non-NULL, it is removed. While it would seem reasonable, the pthread_key_delete function does not automatically call the key 's associated destructor function. If the function is passed an invalid key , it returns the value EINVAL (22); otherwise, it returns a 0, indicating successful removal of the key .
TSD is manipulated with the pthread_setspecific and pthread_getspecific library calls (Table 11.31).
Both functions accept a globally allocated key argument. The key , created by a call to pthread_key_create , is used by the pthread_setspecific library function to store the data referenced by the pointer argument. By definition, each calling thread can bind a different data value with the key . Most often, pointer references memory that has been dynamically allocated by the calling thread. Once bound, the associated values are individually maintained on a per-thread basis. The pthead_setspecific function will fail and return the value ENOMEM (12) if there is insufficient memory to associate a value with a key. If successful, pthread_setspecific returns a 0. The pthread_getspecific library function uses the key argument to retrieve (return) a reference to the TSD. If the key is not bound, a NULL (0) is returned.
Table 11.31. The TSD manipulation Library Functions.
Include File(s) |
Manual Section |
3 |
||
Summary |
int pthread_setspecific(pthread_key_t key, const void *pointer); void *pthread_getspecific(pthread_key_t key); |
|||
Return |
Success |
Failure |
Sets errno |
|
-1 |
Program 11.9 demonstrates one approach for using TSD.
Program 11.9 Using TSD.
File : p11.9.cxx /* Using thread specific data */ #define _GNU_SOURCE + #define _REENTRANT #include #include #include #include 10 using namespace std; const int MAX=20; void *TSD( int ), // manipulates TSD free_me( void * ); // destructor static pthread_key_t key; // global TSD key + int main( int argc, char *argv[] ) { pthread_t thr_id[MAX]; int inc; if ( argc < 2 atoi(argv[1]) > MAX){ 20 cerr << *argv << " num_threads" << endl; return 1; } // generate key (once) pthread_key_create(&key, (void(*)(void*))free_me); + for(int i=0; i < atoi(argv[1]); ++i){ inc = i+1; // can't cast an expr if (pthread_create(&thr_id[i],NULL,(void *(*)(void *))TSD, (void *)inc) > 0){ cerr << "pthread_create failure" << endl; return 2; 30 } } // wait for all threads for(int i=0; i < argc-1; ++i) pthread_join(thr_id[i], NULL); + sleep( 1 ); return 0; } /* TSD routine - passed a value that it will keep private 40 */ void * TSD( int private_stuff ){ static pthread_mutex_t the_lock; void *tsd = NULL; + tsd = pthread_getspecific(key); // initially NULL if (tsd == NULL) { tsd = new pthread_key_t; // create storage tsd = &private_stuff; // make the association pthread_setspecific(key, tsd); 50 cout << pthread_self( ) << " TSD starts at " << *(int *)pthread_getspecific(key) << endl; } for( int i=0; i < 3; ++i ){ sleep(1); + pthread_mutex_lock(&the_lock); // enter critical region cout << pthread_self( ) << " incrementing" << endl; *(int *)pthread_getspecific(key) *= 2; // double private value cout << pthread_self( ) << " yielding" << endl; pthread_mutex_unlock(&the_lock); // exit critical region 60 sched_yield(); // notify scheduler } cout << pthread_self( ) << " TSD finishes at " << *(int *)pthread_getspecific(key) << endl; cout.flush( ); + pthread_exit(NULL); return NULL; } /* Dummy destructor routine 70 */ void free_me( void *value ){ cout << pthread_self( ) << " free reference to " << *(int *) value << endl; + }
The prototype for the user -defined function TSD that will manipulate the TSD and the user-defined function free_me that will act as a destructor are placed prior to main . In addition, the key that will be used to access TSD is allocated prior to main . Its placement assures it will be global in scope. In main , a call is made to pthread_key_create (line 24), and the addresses of the key and the destructor function are passed. A cast is used to keep the compiler from complaining about argument type mismatch. In the first for loop in main , the value of the loop counter plus one is assigned to the variable inc . [20] As each thread is created, it is passed a reference to the user-defined TSD function and the value stored in inc . Again, the cast operator is used to keep the compiler from flagging what it would consider to be mismatched arguments. Once the threads have been generated, main uses a second for loop with a call to pthread_join to wait for the threads to finish their activities. A call to sleep follows the pthread_join loop to allow the final terminating thread sufficient time to flush its output buffer.
[20] Note that the inc variable is used as a temporay storage location, as in this setting it is not legal to pass a reference to an expression.
In the user-defined function TSD a mutex, the_lock , and a void pointer, tsd , are allocated. The storage class for the_lock is static , while the storage class for tsd is auto (the default). A call is made to pthread_getspecific , and the value returned is assigned to tsd . If the key passed to pthread_getspecific has not been associated with a TSD value, the pthread_getspecific call will return a NULL value. If the returned value is NULL (which it should be upon initial entry), a storage location is allocated using the new operator (line 47). The TSD is associated with the key using the pthread_setspecific library function. Next , a for loop is used to simulate activity. Within the loop, a mutex called the_lock is used to bracket a section of code where we would like to keep our display messages from being interleaved with those of other threads that will be executing the same code. In this section of code the TSD is multiplied by 2. A cast is used to coerce the void * to an int * . A call to library function sched_yield causes the current thread to yield its execution to another thread with the same or greater priority. At different points within the program, informational messages tagged with the thread ID are displayed.
A run of Program 11.9 is shown in Figure 11.16.
Figure 11.16 A run of Program 11.9 with four competing threads.
1026 TSD starts at 1 <-- 1 2051 TSD starts at 2 3076 TSD starts at 3 4101 TSD starts at 4 1026 incrementing 1026 yielding 4101 incrementing 4101 yielding 3076 incrementing 3076 yielding 2051 incrementing <-- 2 2051 yielding 1026 incrementing 1026 yielding 4101 incrementing 4101 yielding 3076 incrementing 3076 yielding 2051 incrementing 2051 yielding 1026 incrementing 1026 yielding 1026 TSD finishes at 8 <-- 3 1026 free reference to 8 4101 incrementing 4101 yielding 3076 incrementing 3076 yielding 2051 incrementing 2051 yielding 4101 TSD finishes at 32 4101 free reference to 32 3076 TSD finishes at 24 3076 free reference to 24 2051 TSD finishes at 16 2051 free reference to 16
(1) Each thread has a different starting value for its data.
(2) Each thread accesses its private data, increments it and then yields three times for each tread.
(3) As each thread finishes, it releases its reference to the TSD. The order of processing is determined by the scheduler and may vary across multiple invocations.
As can be seen in Figure 11.16, each thread maintains its own TSD value. The call to the destructor function free_me is made as each thread finishes its execution.
In Program 11.9 we allocated the key for the TSD prior to main and created the key in main . We can, if we are careful, allocate and create the key within the code segment shared by multiple threads. For example, in Program 11.9 we can remove (comment out) the statement prior to main that allocates the key (line 14)
// static pthread_key_t key;
and the pthread_key_create statement (line 24) within main
// pthread_key_create(&key, (void(*)(void*))free_me);
and add the following statements (lines 45 to 53) to the user-defined TSD function.
/* TSD routine - passed a value that it will keep private 40 */ void * TSD( int private_stuff ){ static pthread_mutex_t the_lock; void *tsd = NULL; + // do once static pthread_key_t key; static int done_once = 0; if ( !done_once ) { pthread_mutex_lock(&the_lock); // bracket code 50 if ( !done_once++ ) // re-check & inc pthread_key_create(&key, (void(*)(void*))free_me); pthread_mutex_unlock(&the_lock); // end bracket } tsd = pthread_getspecific(key); // initially NULL
In this second approach the storage class qualifier static is used in the user-defined TSD function when allocating the key . An integer flag variable called done_once , specified as static , is also allocated and initially set to 0 (which with the static qualifier should be its default value anyway). The mutex is used to bracket the inner check of the content of the done_once variable. Only one thread will find the done_once variable at 0, increment the variable, and create the key . After the done_once variable is incremented, the outer if will prevent further access to this section of code. This is by no means the only way in which this can be done. Another approach is to use the pthread_once library function. [21]
[21] If this were a text on just threads, an examination of pthread_once would be in order. However, as this is only an introduction, we will skip the details concerning pthread_once at this time and direct the interested reader to the associated manual page on pthread_once .
This section ends with a final program example, Program 11.10, that uses TSD and incorporates a number of the previously discussed thread- related functions. In this example the program is passed the name of a file to store its output and the number of threads (110) to generate. Each thread, represented by a different uppercase letter of the alphabet, is responsible for moving about within a common two-dimensional grid. As the thread moves, its path is displayed on the screen. The starting point for a thread is marked by an @ symbol. When the thread moves to a new location, its previous location is changed to lowercase. As written, a thread can move in one of four directions: left, right, up, or down. Moves that run off the grid are wrapped, in a modular fashion, to the next valid rowcolumn location. If a thread moves to a location that is boxed in (i.e., a location where its neighbors on the left, right, top, and bottom are occupied), the thread expires . The program terminates when all threads have been boxed in.
To make the output a bit more interesting, the current state of the grid is displayed using basic vt100 escape codes. [22] The vt100 escape codes are incorporated in the user-defined functions LOCATE (used to place the cursor at a specific screen coordinate) and CLS (used to clear the screen). The display is updated dynamically, and in vt100 supported settings, produces a rough appearance of animation. The header information for Program 11.10 is placed in the file local_TSD.h . The contents of local_TSD.h are shown in Figure 11.17.
[22] I am aware that using vt100 emulationescape codes will limit the range of platforms on which the example may run. However, as vt100 emulation is fairly ubiquitous, the actual number of platforms excluded should be minimal.
Figure 11.17 The local_TSD.h file for Program 11.10.
File : local_TSD.h /* local header file for example p11.10.cxx */ #define _REENTRANT #define _GNU_SOURCE + #include #include #include #include #include 10 #include #include #include #include #include + #include using namespace std; const int ROW=20, COL=42, MAX=10; const char ESC='3'; inline 20 void LOCATE(int row, int col){ cout << ESC << "[" << row << ";" << col << "H"; } inline void CLS( ){ + LOCATE(1, 1); cout << ESC << "[2J"; } int my_rand(int start, int range){ 30 struct timeval t; gettimeofday(&t, (struct timezone *)NULL); return (int)(start+((float)range * rand_r((unsigned *)&t.tv_usec)) / (RAND_MAX+1.0)); } + typedef struct { int left, right, top, bot; } DIRECTION; static char guys[] = "ABCDEFGHIJ"; int n_dead = 0; 40 char *the_file; pthread_mutex_t scrn_lock; // function prototypes void display_screen(char *); bool boxed(char *, int, int); + void move(char *, int *, int *, char); void neighbors( int , int , DIRECTION * ); void *play( void * );
Along with the vt100 functions, the local_TSD.h file contains a type definition for a DIRECTION structure that will hold the indices of neighbor locations. Within the program the two-dimensional grid is treated as a vector. Five user-defined functions are prototyped. The display_screen function, passed a reference to the grid, displays the current contents of the grid at a set location on the screen. The boxed function, passed a reference to the grid and a row and column location, returns a true if the neighbors of the rowcolumn location are all occupied; otherwise, it returns false. The move function finds and moves to a new location. The move function is passed a reference to the grid and the rowcolumn location, as well as a copy of the current letter associated with a given thread. Upon completion of move , the grid and the rowcolumn location are updated. The neighbors function is passed a copy of a rowcolumn location and returns a DIRECTION structure containing the indices of the neighbor locations. The play function, passed to each thread, serves as a driver routine for the activities of the thread.
Program 11.10 Animating threads.
File : p11.10.cxx /* p11.10.cxx: Thread animation Compile : g++ -o p11.10 p11.10.cxx -lpthread */ + #include "local_TSD.h" int main(int argc, char *argv[]) { char the_screen[ROW][COL]; pthread_t thread_id[MAX]; 10 int fd0, n_threads; struct stat buf; if (argc != 3) { // check cmd line cerr << "Usage " << *argv << " file_name #_threads" << endl; return 1; + } if ((n_threads = atoi(argv[2])) > MAX ) { cerr << "# threads must be < " << MAX+1 << endl; return 2; } 20 setbuf(stdout, NULL); guys[n_threads] = '
File : p11.10.cxx /* p11.10.cxx: Thread animation Compile : g++ -o p11.10 p11.10.cxx -lpthread */ + #include "local_TSD.h" int main(int argc, char *argv[]) { char the_screen[ROW][COL]; pthread_t thread_id[MAX]; 10 int fd0, n_threads; struct stat buf; if (argc != 3) { // check cmd line cerr << "Usage " << *argv << " file_name #_threads" << endl; return 1; + } if ((n_threads = atoi(argv[2])) > MAX ) { cerr << "# threads must be < " << MAX+1 << endl; return 2; } 20 setbuf ( stdout , NULL); guys[n_threads] = '