«  6.4. POSIX Thread Library   ::   Contents   ::   6.6. Implicit Threading and Language-based Threads  »

6.5. Thread Arguments and Return Values

The pthread_create() imposes a strict format on the prototype of the function that will run in the new thread. It must take a single void* parameter and return a single void* value. The last parameter of pthread_create() is passed as the argument to the function, whereas the return value is passed using pthread_exit() and pthread_join(). This section looks at the details of these mechanisms and their implications.

6.5.1. Passing a Single Argument to Threads

Passing a single argument to a thread seems straightforward, but is easy to do incorrectly. As a simple example to illustrate the danger, Code Listing 6.5 is designed to run in a separate thread:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
/* Code Listing 6.5:
   A thread that will print a single integer value
 */

void *
child_thread (void *args)
{
  /* POTENTIALLY DANGEROUS TIMING */
  int *argptr = (int *) args;
  int arg = *argptr;

  /* Print the local copy of the argument */
  printf ("Argument is %d\n", arg);
  pthread_exit (NULL);
}

The danger of this code can be illustrated with the loop in Code Listing 6.6. The intent is to pass the value 1 to the first thread, 2 to the second, and so on. However, it is critical to note that there is only a single copy of the i variable. That is, this code passes the address of the single variable to all 10 threads; the code almost certainly does not pass the intended values.

1
2
3
4
5
6
7
8
/* Code Listing 6.6:
   Passing a pointer to a variable that repeatedly changes is a common error with threads
 */

/* BAD CODE - DON'T DO THIS */
/* What value is actually passed to the thread? */
for (int i = 1; i <= 10; i++)
  assert (pthread_create (&child[i], NULL, child_thread, &i) == 0);

The key problem is that thread creation and execution is asynchronous. That means that it is impossible to predict when each of the new threads start running. One possible timing is that all 10 threads are created first, leading to i storing the value 11. At that point, each of the threads dereference their respective argptr variable and all get the same value of 11.

One common solution to this problem is to cast numeric values as pointers, as shown in Code Listing 6.7. That is, the int i variable gets cast as a (void*) argument in the call to pthread_create(). Then, the void* argument to child_thread() casts the argument back to a int instance.

1
2
3
4
5
6
7
8
/* Code Listing 6.7:
   Each thread should be given a separate value, rather than a shared address
 */

/* FIXED VERSION */
/* ints are passed by value, so a COPY gets passed to each call */
for (int i = 1; i <= 10; i++)
  assert (pthread_create (&child[i], NULL, child_thread, (void *)i) == 0);

What makes this code work is the fact that scalar variables (e.g., int variables) are passed using call-by-value semantics. When this code prepares for the pthread_create() call, a separate copy of the current value of the i variable is placed into a register or onto the stack. Code Listing 6.8 shows the corrected version of Code Listing 6.5. The child_thread() function then gets this copy, regardless of any changes to the original i variable. When the child thread then casts its args parameter to a local arg_value, it is working with the correct value that was passed.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
/* Code Listing 6.8:
   A safer version of Code Listing 6.5
 */

/* Convention: It is common to name a void* parameter with a name
   that begins with _, then cast it to a local variable that has
   the same (or nearly the same) name without the _. So _args will
   become args. Recall that _ has no special meaning and is treated
   like a normal alphabetical character. */

void *
child_thread (void *_args)
{
  /* Safe whenever size of int <= size of pointer (which is
     usually true) */
  int arg = (int) _args;

  /* Print the local copy of the argument */
  printf ("Argument is %ld\n", arg);
  pthread_exit (NULL);
}
Decorative bug warning

Bug Warning


Casting integral values to pointers and back again is a common practice for passing parameters to pthreads. However, while it is generally safe in practice, it is potentially a bug on some platforms. Specifically, this technique relies on the fact that pointers are at least as large as standard integer types. That is, int variables are typically (but not required to be) 32 bits in size. Modern CPU architectures tend to use 32- or 64-bit addresses. As such, casting a 32-bit int up to a void* then back to a 32-bit int is safe.

On the other hand, assume the argument was declared as a long variable instance. If the code is running on a 32-bit architecture (which is not uncommon for virtualized systems) but the long type is 64 bits in size, then half of the argument is lost by down-casting to the pointer for the call to pthread_create()!

6.5.2. Passing Multiple Arguments to Threads

When passing multiple arguments to a child thread, the standard approach is to group the arguments within a struct declaration, as shown in Code Listing 6.9. The address of the struct instance gets passed as the arg to pthread_create(). The new thread’s entry point receives a void* parameter that can then be cast into the struct type.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
/* Code Listing 6.9:
   Passing multiple arguments to a thread requires grouping them into a struct
 */

/* Assume we have:
     struct thread_args {
       int first;
       const char *second;
     };
 */

struct thread_args *args = malloc (sizeof (struct thread_args));
args->first = 5;
args->second = "Hello";

/* Note that the data structure resides on the heap */
assert (pthread_create (&child, NULL, hello_thread, args) == 0);

Code Listing 6.10 shows the new thread receiving the pointer to the struct and freeing the allocated memory when it is finished with the data.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
/* Code Listing 6.10:
   The child thread receives multiple values through the passed struct
 */

/* Using the convention of casting _args to args */
void *
hello_thread (void *_args)
{
  /* Cast args into a meaningful pointer type that we can use */
  struct thread_args *args = (struct thread_args *) _args;
  printf ("First: %d; Second: '%s'\n", args->first, args->second);

  /* Do not forget to free the struct used for arguments */
  free (args);
  pthread_exit (NULL);
}
Decorative bug warning

Bug Warning


A common mistake with passing arguments in this manner is to declare the struct instance as a local variable instead of using dynamic allocation. The problem, again, is the asynchronous nature of pthread_create(). Consider this sample code:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
/* Create a local instance on the current thread's stack */
struct thread_args args;
args.first = 5;
args.second = "Hello";

/* Pass a reference to the local instance */
assert (pthread_create (&child, NULL, hello_thread, &args) == 0);

/* Parent thread exits, but the child may not have run yet */
pthread_exit (NULL);

/* Future references to args are invalid! */

If the child thread runs immediately before pthread_create() returns, then everything would be fine. However, there is no guarantee that this happens. Instead, it is just as likely that pthread_create() returns and the parent thread exits. Once that happens, all data on the parent thread’s stack (including the struct thread_args instance) become invalid. The child thread now has a dangling pointer to potentially corrupted data. This is another example of a race condition that can happen with threads.

6.5.3. Returning Values from Threads

There are three common ways to get return values back from a thread. All three use techniques that are similar to those used for passing arguments. Code Listing 6.11 shows one simple technique, which is to augment the struct declaration to include space for any return values.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
/* Code Listing 6.11:
   Allocating space for a return value as part of the struct passed
 */

/* Approach 1: Include space for return values in the struct */

/* Thread argument struct declaration */
struct numbers {
  int a;
  int b;
  int sum;
};

void *
sum_thread (void *_args)
{
  /* Cast the arguments to the usable struct type */
  struct numbers *args = (struct numbers *) _args;

  /* Place the result into the struct itself (on the heap) */
  args->sum = args->a + args->b;
  pthread_exit (NULL);
}

The child thread receives a pointer to the struct instance, using the input parameters as needed. In this case, the values of a and b are added, and the resulting sum is copied back into the struct. As shown in Code Listing 6.12, the main thread uses pthread_join() to wait until the child thread exits. Once the child finishes, the main thread can retrieve all three values (a, b, and sum) from the struct itself.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
/* Code Listing 6.12:
   The main thread can retrieve the return value from the struct after joining the child thread
 */

/* Allocate and pass a heap instance of the struct type */
struct numbers *args = calloc (sizeof (struct numbers), 1);
args->a = 5;
args->b = 8;

assert (pthread_create (&child, NULL, sum_thread, args) == 0);

/* Wait for the thread to finish */
pthread_join (child, NULL);
/* The struct is still on the heap, so the result is accessible */
printf ("%d + %d = %d\n", args->a, args->b, args->sum);

/* Clean up the struct instance */
free (args);
args = NULL;

There are three key observations about this approach:

  • The main and the child threads have access to both the input and the output. This fact means that the main thread has information about how this particular child thread was invoked. If the main thread is keeping track of many threads, this additional information may be helpful.
  • Responsibility for memory management resides in one location: the main thread. If responsibility is split between the programmer maintaining the main thread and the programmer maintaining the child thread, there is the possibility for miscommunication leading to memory leaks (or worse, premature de-allocation).
  • The major disadvantage of this approach is that the input parameters may be kept on the heap for much longer than needed, particularly if the child thread runs for a significant amount of time.

Code Listing 6.13 shows an alternative approach for simple scalar return types, which is to reuse the trick of casting to and from the void* type. When a thread calls pthread_exit(), it can specify a pointer to return as an argument.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
/* Code Listing 6.13:
   A second technique to return a value is to pass it as the thread’s exit code
 */

/* Approach 2: Scalar return types as void* with pthread_exit() */

/* Thread argument struct contains only input parameters */
struct numbers {
  int a;
  int b;
};

void *
sum_thread (void *_args)
{
  /* Cast the argument to the usable struct */
  struct numbers *args = (struct numbers *) _args;

  /* Pass the result back by casting it to the void* */
  pthread_exit ((void *) (args->a + args->b));
}

Code Listing 6.14 shows how the main thread calls pthread_join() to retrieve the pointer. Unless the thread has been detached (or it was created with the PTHREAD_CREATE_DETACHED attribute), the pointer returned with pthread_exit() will remain associated with the thread until it is joined.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
/* Code Listing 6.14:
   Retrieving a thread’s exit code when it is joined
 */

/* Allocate the struct like before and pass it to the thread */
struct numbers *args = calloc (sizeof (struct numbers), 1);
args->a = 5;
args->b = 8;

assert (pthread_create (&child, NULL, sum_thread, args) == 0);

/* Wait for thread to finish and retrieve the void* into sum */
void *sum = NULL;
pthread_join (child, &sum);
printf ("Sum: %d\n", (int) sum);

free (args);
args == NULL;

Code Listing 6.15 shows a third approach to returning values from the thread. In this style, the child thread allocates a separate struct dynamically to hold the return values. This technique allows a thread to return multiple values rather than a single scalar. For instance, consider the following calculator thread. It receives two int values as input and returns the results of five simple arithmetic operations.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
/* Code Listing 6.15:
   The child can dynamically allocate space for the return values
 */

/* Approach 3: Allocate separate struct for return values */

/* struct for passing arguments to the child thread */
struct args {
  int a;
  int b;
};

/* struct for returning results from the child thread */
struct results {
  int sum;
  int difference;
  int product;
  int quotient;
  int modulus;
};

void *
calculator (void *_args)
{
  /* Cast the args to the usable struct type */
  struct args *args = (struct args *) _args;

  /* Allocate heap space for this thread's results */
  struct results *results = calloc (sizeof (struct results), 1);
  results->sum        = args->a + args->b;
  results->difference = args->a - args->b;
  results->product    = args->a * args->b;
  results->quotient   = args->a / args->b;
  results->modulus    = args->a % args->b;
  /* De-allocate the input instance and return the pointer to
     results on heap */
  free (args);
  pthread_exit (results);
}

It is critical to note that the struct instance here must be allocated dynamically. Once the thread calls pthread_exit(), everything on its stack becomes invalid. A thread should never pass a pointer to a local variable with pthread_exit().

Retrieving the returned data can be accomplished with pthread_join(). In the following example, the main thread creates five separate instances of the calculator thread. Each of these child threads gets a pointer to a unique struct args instance with the corresponding parameters. Each child then allocates its own struct results instance on the heap. This allows the data to persist after the thread has finished. In Code Listing 6.14, the main thread gets each thread’s pointer one at a time, with a separate call to pthread_join(). Since the child thread has already finished at this point, the main thread must bear the responsibility for calling free() to de-allocate the struct results instance.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
/* Code Listing 6.16:
   The main thread passes arguments to the child threads and frees the results
 */

/* Create 5 threads, each calling calculator() */
pthread_t child[5];

/* Allocate arguments and create the threads */
struct args *args[5] = { NULL, NULL, NULL, NULL, NULL };
for (int i = 0; i < 5; i++)
  {
    /* args[i] is a pointer to the arguments for thread i */
    args[i] = calloc (sizeof (struct args), 1);

    /* thread 0 calls calculator(1,1)
       thread 1 calls calculator(2,4)
       thread 2 calls calculator(3,9)
       and so on... */
    args[i]->a = i + 1;
    args[i]->b = (i + 1) * (i + 1);
    assert (pthread_create (&child[i], NULL, calculator, args[i]) 
            == 0);
  }

/* Allocate an array of pointers to result structs */
struct results *results[5];
for (int i = 0; i < 5; i++)
  {
    /* Passing results[i] by reference creates (void **) */
    pthread_join (child[i], (void **)&results[i]);

    /* Print each of the results and free the struct */
    printf ("Calculator (%d, %2d) ==> ", i+1, (i+1) * (i+1));
    printf ("+:%3d;   ", results[i]->sum);
    printf ("-:%3d;   ", results[i]->difference);
    printf ("*:%3d;   ", results[i]->product);
    printf ("/:%3d;   ", results[i]->quotient);
    printf ("%%:%3d\n", results[i]->modulus);
    free (results[i]);
  }
Decorative bug warning

Bug Warning


All of the functions for creating threads, passing arguments, and getting return values involve a lot of pointers. Furthermore, the pointers are dereferenced and manipulated asynchronously because of the nature of multithreading. It is vital to remember the types and lifetimes of each pointer and the corresponding data structure.

  • The first parameter for pthread_create() is a pthread_t*. The argument should typically be an existing pthread_t passed by reference with the & operator.
  • The final parameter to pthread_create() must either be a scalar (cast as a pointer) or a pointer to data that persists until the child thread runs. That is, the target of the pointer must not be modified by the main thread until the child thread has been joined (to guarantee the child has run).
  • The parameter to pthread_exit() must be a scalar value (cast as a pointer) or a pointer to non-stack data. The data must be guaranteed to be valid even after the thread has been completely destroyed.
  • The final parameter to pthread_join() must be a pointer that is passed by reference. That is, pthread_join() will change this pointer to point to the returned data structure.
«  6.4. POSIX Thread Library   ::   Contents   ::   6.6. Implicit Threading and Language-based Threads  »

Contact Us License