Applying C - Pthreads
Written by Harry Fairhead   
Monday, 25 September 2023
Article Index
Applying C - Pthreads
Pthreads
Joinable And Detached Threads
Thread Local

It is important to understand what threads share and what constitutes their own “property”. When a thread is created it gets its own use of the CPU registers, its own stack, signal mask and priority. It shares open files, user id and group, signals and signal handlers and all non-local variables. Notice that each thread you start gets its own stack and this means that each thread will consume a larger amount of memory than you might imagine – 10Mbytes or more per thread.

One way of thinking about this is that the function that is being executed has its own local variables which are allocated on its own stack, but it has access to all of the variables that it would have access to if it was running on the same thread as the main program. That is, it has access to any variables allocated on the heap and variables with global scope.

The next question is what happens to all of the thread’s resources when it terminates? By default threads are created as “joinable” and in this case the thread’s resources are kept after the thread terminates. You can also create “detached” threads which automatically clear up their resources when they terminate.

Joinable threads can be the source of memory leaks if the process goes on running and keeps starting threads which terminate. Each terminated joinable thread keeps its stack allocation, for example. Of course, all resources are reclaimed when the process ends. To free the resources of a joinable thread you have to join it. A thread can call the join function to wait for another thread to complete. The join function also returns the thread’s return value which is kept after the thread has terminated. You can think of joinable threads as keeping their state intact until some other thread wants to make use of their data, after which their resources are deallocated.

In:

int pthread_join(thread,**returnval);

the first parameter is a pthread_t, which identifies the thread to be joined and the second parameter is a pointer to a pointer to the returned value.

Notice that if thread has already terminated then join returns at once with the returnval pointer. If the join returns without an error you can be sure that the thread has terminated and that you have its returnval.

If multiple threads try to join to the same thread at the same time the result is undefined – simply because one of the joins will be executed first and the thread’s resources, including its return value, will be removed.

Notice that a thread can join multiple threads and wait for them to complete one after another:

int pthread_join(thread1,**returnval);
int pthread_join(thread2,**returnval);
int pthread_join(thread3,**returnval);

This first waits for thread1 to terminate, then waits for thread2, and finally for thread3. Notice that this still works if thread2 or thread3 terminate before thread1.

For an example of using pthread_join see the next section.

This leaves open the question of how you implement more complicated waiting for threads, such as waiting for the first thread in a group to finish, no matter which one it is – see later.

A detached thread removes its resources as soon as it exits and hence it cannot be the subject of a join. You can change a default joinable thread into a detached thread using:

int pthread_detach(thread);

where thread is a pthread_t that identifies the thread.

If you want to create detached threads rather than convert a joinable thread you need to use the thread attributes object and the setdetachstate function:

pthread_attr_t attrObj;
pthread_attr_init(&attrObj);
pthread_attr_setdetachstate(&attrObj, PTHREAD_CREATE_DETACHED);    
pthread_t pthread;
int param = 0;
int id = pthread_create(&pthread, &attrObj, hello, &param);

The thread so created using attrObj is detached and you will get an error if you try to join it. You can also use PTHREAD_CREATE_JOINABLE to create a joinable thread, but this is the default.

The distinction between joinable and detached threads is initially puzzling, but the reasons for using each type is very clear:

  • If you want to get a value back from a thread then make it joinable.

  • If you create a joinable thread then join it, otherwise you risk creating a zombie thread that holds on to its resources.

  • The only way a joinable thread releases its resources is if it is joined or if the process terminates.

  • Create a detached thread whenever you don’t want to use the thread’s returned value.

Threads and Scope - Thread Local

A subject that is often ignored in introductions to threads is which variables are accessible from any particular thread. Which variables can a thread access is very simple because it has the same answer in a single threaded program. A thread can access the local variables of any function it executes. A thread executing a different function cannot access the local variables in any other function. That is, local variables are still local when functions are executed by multiple threads. Notice that each thread has its own stack and hence any local variables it creates are accessible only by it.

Global variables, however, are another matter. A global variable can be accessed from any function in the program unless a local variable of the same name is declared. In most cases a global variable is a way of sharing information between functions. For example, a global state variable can be used to make sure that all functions have access to and can modify the current state indicator. Global variables are shared between all of the functions in a program. If any of those functions are executed by another thread then we immediately have the problem of race conditions - two threads might try to modify the shared data at the same time with the result that the final value might be wrong or inconsistent. The solution, of course, is to use locks to restrict access and this is the topic of the next section.

Global variables are resources shared between all threads. Often in a program functions form groups that are intended to be executed on the same thread. For example, a group of functions might implement a state machine with one function per state. Which one is called might be controlled by a state variable which needs to be shared by all of the functions in the group - i.e. it needs to be global. However, the state variable does not need to be shared by other threads executing the same group of functions. In other words, you may have multiple state machines running at the same time using the same group of functions each with its own thread. The only problem is that the global state variable is shared by all of the threads and so all of the state machines have the same state!

What is needed is a way to create a variable that is global to all of the functions but not shared between threads - this is a thread local storage variable. Thread local storage provides global variables that are accessible from all of the functions in a program but not shared between threads - each thread has its own copy of the global variable.



Last Updated ( Tuesday, 26 September 2023 )