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.
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);
}
|
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()
!
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);
}
|
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.
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]);
}
|
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 apthread_t*
. The argument should typically be an existingpthread_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.