Joining threads

Joining a thread means waiting for it to terminate, which can be seen as a specific usage of condition variables.

Waiting for a thread

Using the pthread_join subroutine alows a thread to wait for another thread to terminate. More complex conditions, such as waiting for multiple threads to terminate, can be implemented by using condition variables.

Calling the pthread_join subroutine

The pthread_join subroutine blocks the calling thread until the specified thread terminates. The target thread (the thread whose termination is awaited) must not be detached. If the target thread is already terminated, but not detached, the pthread_join subroutine returns immediately. After a target thread has been joined, it is automatically detached, and its storage can be reclaimed.

The following table indicates the possible cases when a thread calls the pthread_join subroutine, depending on the state and the detachstate attribute of the target thread.

Target State Undetached target Detached target
Target is still running The caller is blocked until the target is terminated. The call returns immediately, indicating an error.
Target is terminated The call returns immediately, indicating a successful completion.  

Multiple joins

Several threads can join the same target thread, if the target is not detached. The success of this operation depends on the order of the calls to the pthread_join subroutine and the moment when the target thread terminates.

  • Any call to the pthread_join subroutine occurring before the target thread's termination blocks the calling thread.
  • When the target thread terminates, all blocked threads are awoken, and the target thread is automatically detached.
  • Any call to the pthread_join subroutine occurring after the target thread's termination will fail, because the thread is detached by the previous join.
  • If no thread called the pthread_join subroutine before the target thread's termination, the first call to the pthread_join subroutine will return immediately, indicating a successful completion, and any further call will fail.

Join example

In the following example, the program ends after exactly five messages display in each language. This is done by blocking the initial thread until the "writer" threads exit.
#include <pthread.h>    /* include file for pthreads - the 1st */
#include <stdio.h>      /* include file for printf()           */

void *Thread(void *string)
{
        int i;
 
        /* writes five messages and exits */
        for (i=0; i<5; i++)
                printf("%s\n", (char *)string);
        pthread_exit(NULL);
}

int main()
{
        char *e_str = "Hello!";
        char *f_str = "Bonjour !";
 
        pthread_attr_t attr;
        pthread_t e_th;
        pthread_t f_th;
 
        int rc;

        /* creates the right attribute */
        pthread_attr_init(&attr);
        pthread_attr_setdetachstate(&attr,
                PTHREAD_CREATE_UNDETACHED);

        /* creates both threads */
        rc = pthread_create(&e_th, &attr, Thread, (void *)e_str);
        if (rc)
                exit(-1);
        rc = pthread_create(&f_th, &attr, Thread, (void *)f_str);
        if (rc)
                exit(-1);
        pthread_attr_destroy(&attr);

        /* joins the threads */
        pthread_join(e_th, NULL);
        pthread_join(f_th, NULL);
 
        pthread_exit(NULL);
}

A thread cannot join itself because a deadlock would occur and it is detected by the library. However, two threads may try to join each other. They will deadlock, but this situation is not detected by the library.

Returning information from a thread

The pthread_join subroutine also allows a thread to return information to another thread. When a thread calls the pthread_exit subroutine or when it returns from its entry-point routine, it returns a pointer (see Exiting a Thread). This pointer is stored as long as the thread is not detached, and the pthread_join subroutine can return it.

For example, a multithreaded grep command may choose the implementation in the following example. In this example, the initial thread creates one thread per file to scan, each thread having the same entry point routine. The initial thread then waits for all threads to be terminated. Each "scanning" thread stores the found lines in a dynamically allocated buffer and returns a pointer to this buffer. The initial thread prints each buffer and releases it.
/* "scanning" thread */
...
buffer = malloc(...);
        /* finds the search pattern in the file 
           and stores the lines in the buffer   */
return (buffer);

/* initial thread */
...
for (/* each created thread */) {
        void *buf;
        pthread_join(thread, &buf);
        if (buf != NULL) {
                /* print all the lines in the buffer,
                        preceded by the filename of the thread */
                free(buf);
        }
}
...

If the target thread is canceled, the pthread_join subroutine returns a value of -1 cast into a pointer (see Canceling a Thread). Because -1 cannot be a pointer value, getting -1 as returned pointer from a thread means that the thread was canceled.

The returned pointer can point to any kind of data. The pointer must still be valid after the thread was terminated and its storage reclaimed. Therefore, avoid returning a value, because the destructor routine is called when the thread's storage is reclaimed.

Returning a pointer to dynamically allocated storage to several threads needs special consideration. Consider the following code fragment:
void *returned_data;
...
pthread_join(target_thread, &returned_data);
/* retrieves information from returned_data */
free(returned_data);
The returned_data pointer is freed when it is executed by only one thread. If several threads execute the above code fragment concurrently, the returned_data pointer is freed several times; a situation that must be avoided. To prevent this, use a mutex-protected flag to signal that the returned_data pointer was freed. The following line from the previous example:
free(returned_data);
would be replaced by the following lines, where a mutex can be used for locking the access to the critical region (assuming the flag variable is initially 0):
/* lock - entering a critical region, no other thread should
                run this portion of code concurrently */
if (!flag) {
        free(returned_data);
        flag = 1;
}
/* unlock - exiting the critical region */

Locking access to the critical region ensures that the returned_data pointer is freed only once.

When returning a pointer to dynamically allocated storage to several threads all executing different code, you must ensure that exactly one thread frees the pointer.