Modern computers are based on multiprocessor architectures. Each “core” can execute only one instruction at a time, but all the processing cores combined can execute multiple instructions simultaneously.

However, program code that’s written in the traditional way is synchronous; that is the instructions follow one another in a logical and sequential manner. And it adds to that, the code needs to be executed in that specific order for the program to be useful to the end-user, otherwise the program would be chaotic and would produce unexpected results. Therefore, it’s not possible for the Operating System to simply split a program into equal parts at run-time and execute each part on a different processing core. It would not work.

In order for a program to be able to take advantage of the extra processing cores on a computer, we need to structure our code into independent executable units, such that each independent unit can be executed on a different core, without affecting the logic of the program. That’s where the concept of threads come into play.

What’s a Thread?

While a process can be thought of as an instance of a program,

a thread is a segment of program code that can be executed independently and concurrently with respect to other parts of the program.

In layman’s terms, a thread is basically a “procedure” that can be executed independently, either concurrently or at a later stage as deemed appropriate by the scheduler. Therefore, multi-threading is a programming model that can be used to implement parallelism in software.

A process can contain multiple threads, all sharing the same address space and resources of the process they belong to. However, the following are specific to each thread:

  • Registers
  • Stack Pointer
  • Program Counter
  • Signal Mask
  • Scheduling priorities/policies
  • Other thread specific data

The Need for POSIX Threads (pthreads)

Due to human ingenuity and originality, computer manufacturers have written their own implementation of threads. Because these vendor-specific implementations were all different from each other, programs written using a particular implementation of threads would not work on another vendor’s machine. This made it tedious for programmers to port their code to another platform where a different implementation of threads was being used.

In order to solve this problem, a standardized API was needed so that multi-threaded code written for one platform would also work on another. On UNIX systems, this API is known as POSIX.1c, Threads extensions (IEEE Std 1003.1c-1995), a.k.a POSIX threads, or simply pthreads.

The pthread API

The pthread API is defined in the C programming language and is implemented via the pthread.h header file and libpthread library on POSIX-compliant UNIX-like systems. The pthread API contains several procedures and data types that are related to the creation and management of threads.

Spawning a Thread

Spawning is just a fancy name that refers to creating a thread. Let’s see how to create/spawn an actual thread using the pthread API !

First, we need to include the pthread.h header file in our code to be able to use the pthead API. Then, we need a function that returns a void pointer and accepts a void pointer as its only argument. It’s important to declare the thread function as such.

#include <stdio.h>
#include <pthread.h>

void *thread_function(void *arg)
{
    printf("From Thread: Hello World! \n");
}

int main()
{
    
}

So far, we have only defined our thread function, and specified what it should do. In this case, our thread simply displays the line “From Thread: Hello World!”. Now, we need to write the code that spawns the thread.

We need a variable of type pthread_t to store the ID of the thread to be created. Next, the function pthread_create() is used to spawn the thread. The prototype for pthread_create() is shown below.

int pthread_create(pthread_t *tid, const pthread_attr_t *attr, void *(*start_routine) (void *), void *arg);

*tid is a pointer to a variable of type pthread_t that stores the ID of the thread.

*attr is a pointer to a structure of type pthread_attr_t that specifies the attributes to be used when creating the thread. Setting this to NULL will create a thread with default attributes.

*(*start_routine) is the entry point of the thread function.

*arg is a void pointer to the argument to be passed to the thread function. POSIX threads can accept only one argument. Set this to NULL if there is no argument to be passed.

#include <stdio.h>
#include <pthread.h>

void *thread_function(void *arg)
{
    printf("Thread: Hello World! \n");
}

int main()
{
    pthread_t thread_id;

    pthread_create(&thread_id, NULL, thread_function, NULL);

    // Problem: How do we tell main() to wait for our thread
    // to execute ?
    return 0; 
}

The code above will compile just fine. However, the program will not work properly. That’s because when the main thread returns from main(), it will cause all other threads to terminate, even if they are still in execution. This is an undesirable behavior since we would want our threads to be executed completely before exiting the program.

In order to achieve this, we need a mechanism that allows main() to check if some threads are still running, and to wait for them to finish executing before exiting the main() function. Such a mechanism can be implemented as follows:

Instead of putting return 0; as the last statement in main(), we terminate the main() function with pthread_exit(NULL); .

When pthread_exit(NULL); is the last statement in main(), it will cause the main() function to wait until all other threads have completed. This ensures that the child threads execute completely.

/*
* Project: Pthreads Tutorial
* File: pthreads_hello.c
* Date: February 19, 2022
* Author: M. Ray Auhammud
* License: MIT License
*/

#include <stdio.h>
#include <pthread.h>

void *thread_function(void *arg);

int main()
{
    pthread_t thread_id;

    pthread_create(&thread_id, NULL, thread_function, NULL);

    printf("From main: Hello World !\n");

    printf("Exiting: main\n");

    // This should be the last statement in main()
    pthread_exit(NULL);
}

void *thread_function(void *arg)
{
    printf("From thread: Hello World !\n");
    
    printf("Exiting: thread\n");
}

The above code can be compiled on Linux as follows:

gcc pthreads_hello.c -lpthread -o pthreads_hello

Passing a Simple Argument

In the previous example, we spawned a thread without any argument. Now we’ll see how to pass an argument to a thread. But first let’s check the prototype for pthread_create() again.

int pthread_create(pthread_t *tid, const pthread_attr_t *attr, void *(*start_routine) (void *), void *arg);

As we have seen earlier,

*arg is a void pointer to the argument to be passed to the thread function. POSIX threads can accept only one argument. Set this to NULL if there is no argument to be be passed.

This means instead of passing the actual argument, we need to pass a pointer of type void to the thread via the pthread_create() function.

To get a void pointer to the argument, we use the reference operator & to first get an integer pointer to the argument, and then cast it to a void pointer using (void *) . For example: (void *)&age , where age is an integer that we are passing to the thread.

Because we passed a void pointer to the thread, we can’t access the value of age directly from the thread. First, we need to cast the void pointer back into an integer pointer using (int *) , and then de-reference the integer pointer using the * operator to get the actual value of age. For example:

int age = *(int *)arg;

A concrete example is shown below.

/*
* Project: Pthreads Tutorial
* File: pthreads_arg.c
* Date: February 19, 2022
* Author: M. Ray Auhammud
* License: MIT License
*/

#include <stdio.h>
#include <pthread.h>

void *displayAge(void *arg);

int main()
{
    int age;

    // Ask user to provide an age
    printf("Enter age: ");
    scanf("%d", &age);

    // Spawn the displayAge thread with age as argument
    pthread_t displayAge_id;
    pthread_create(&displayAge_id, NULL, displayAge, (void *)&age);

    // This should be the last statement in main()
    pthread_exit(NULL);
}

void *displayAge(void *arg)
{
    // Get the actual value of age
    int age = *(int *)arg;

    // Display the age
    printf("Age entered: %d \n", age);
}

Just like before, the code can be compiled as follows:

gcc pthreads_arg.c -lpthread -o pthreads_arg

Passing Several Complex Arguments

More often than not, we need to pass multi-dimensional arrays or more than one argument to a thread. However, the pthreads API allows us to create threads having only one argument. In order to circumvent this “limitation”, we should create a structure containing the arguments and pass it to the thread. Such a structure can elegantly be defined as follows:

typedef struct _args_displayInfo
{
    char name[3][21];
    char surname[3][21];
    int age[3];
} args_displayInfo;

The code above defines a structure with 3 elements: name, surname, age. Each of these elements is an array.

The typedef keyword at the beginning of the structure definition allows us to refer to the structure as a custom data type. In this case, we can refer to the structure as if it’s of the type args_displayInfo instead of struct _args_displayInfo , making it easier for us to work with it.

name and surname are 2D arrays of type char . They can each store 3 strings of 21 characters, including the null terminator.

age is a 1D array that can store 3 integers.

After having defined the composition of the structure, we can declare a structure of type args_displayInfo called params and assign values to its elements as follows:

// Create an instance of the arguments structure
args_displayInfo params;

// Read in values in the arguments structure
for (int i = 0; i < 3; i++)
{
    // Ask user to provide a name
    printf("Enter name: ");
    scanf("%s", &params.name[i]);

    // Ask user to provide a surname
    printf("Enter surname: ");
    scanf("%s", &params.surname[i]);

    // Ask user to provide an age
    printf("Enter age: ");
    scanf("%d", &params.age[i]);

    printf("\n");
}

We can now pass the params structure as an argument to the thread. Here is the complete code for convenience:

/*
* Project: Pthreads Tutorial
* File: pthreads_arg_complex.c
* Date: February 19, 2022
* Author: M. Ray Auhammud
* License: MIT License
*/

#include <stdio.h>
#include <pthread.h>

void *displayInfo(void *arg);

typedef struct _args_displayInfo
{
    char name[3][21];
    char surname[3][21];
    int age[3];
} args_displayInfo;

int main()
{
    // Create an instance of the arguments structure
    args_displayInfo params;

    // Read in values in the arguments structure
    for (int i = 0; i < 3; i++)
    {
        // Ask user to provide a name
        printf("Enter name: ");
        scanf("%s", &params.name[i]);

        // Ask user to provide a surname
        printf("Enter surname: ");
        scanf("%s", &params.surname[i]);

        // Ask user to provide an age
        printf("Enter age: ");
        scanf("%d", &params.age[i]);

        printf("\n");
    }

    // Spawn the displayInfo thread with 'params' as argument
    pthread_t displayInfo_id;
    pthread_create(&displayInfo_id, NULL, displayInfo, (void *)&params);

    // This should be the last statement in main()
    pthread_exit(NULL);
}

void *displayInfo(void *arg)
{
    // Get the actual arguments structure
    args_displayInfo params = *(args_displayInfo *)arg;

    // Display the values from the arguments structure
    for (int i = 0; i < 3; i++)
    {
        printf("Full Name: %s %s\n", params.name[i], params.surname[i]);        
        printf("Age: %d \n", params.age[i]);

        printf("\n");
    }
}

Have Fun, Be Thread-Safe

Having fun with threads so far? Well, I am :P. Until now, we have spawned only 1 thread with several parameters by bundling the parameters into a structure and passing a pointer to the structure as an argument to the thread.

Suppose we now have 2 child threads:

// Create an instance of the arguments structure
args_displayInfo params;

/*
Code to read in values in the arguments structure goes here
*/

// Spawn the displayInfo thread with 'params' as argument
pthread_t displayInfo_id;
pthread_create(&displayInfo_id, NULL, displayInfo, (void *)&params);

// Spawn the displayData thread with 'params' as argument
pthread_t displayData_id;
pthread_create(&displayData_id, NULL, displayInfo, (void *)&params);

Notice what we did here? We have declared only 1 instance of the argument structure, and we have passed the same instance to 2 different threads. Doing something like this is totally legal, but unsafe. The code above is said to not be thread-safe.

The reason why such a practice is not recommended is because both threads are sharing the same pointer to the argument. Therefore, each thread is free to modify the contents of the argument at its whims. This can lead to scenarios where the integrity of the argument cannot be guaranteed since either thread can change the value of the argument. In order to make the code thread-safe, a separate copy of the argument should be created and passed to each thread as shown below:

// Create 2 instances of the arguments structure
args_displayInfo params1;
args_displayInfo params2;

// Code to read in values in the params1 structure goes here
// Code to read in values in the params2 structure goes here

// Spawn the displayInfo thread with 'params1' as argument
pthread_t displayInfo_id;
pthread_create(&displayInfo_id, NULL, displayInfo, (void *)&params1);

// Spawn the displayData thread with 'params2' as argument
pthread_t displayData_id;
pthread_create(&displayData_id, NULL, displayInfo, (void *)&params2);

The code above is now thread-safe since each thread can modify the argument being passed to it without affecting the argument of other threads.

Returning a Value from a Thread

join posix pthreads

Image courtesy of University of Bologna

All functions can return a value and threads are no exception. With the pthreads API, returning a value from a thread is achieved using two functions: pthread_join() and pthread_exit(). The prototype for each function is given below:

pthread_join()

int pthread_join(pthread_t tid, void **retval);

tid is a variable of type pthread_t that contains the ID of the thread from which we want to get a return value.

**retval is a pointer to a void pointer to the return value.

pthread_exit()

void pthread_exit(void *retval);

*retval is a void pointer to the value to be returned from the thread.

Suppose we have a thread function calcAdd that adds 2 numbers specified in its structure argument and returns the sum of the 2 numbers.

void *calcAdd(void *arg)
{
    // Get the actual arguments structure
    args_calcAdd params = *(args_calcAdd *)arg;

    // Declare pointer to the return value
    // and allocate memory at that address
    int *sum = (int *)malloc(sizeof(int));

    // Perform the addition and store the results
    // at location pointed to by sum
    *sum = params.num1 + params.num2;

    // Return void pointer to the value we want to be returned
    pthread_exit((void *)sum);
}

Since we need to return a void pointer to the value to be returned, we first declare an integer pointer and allocates memory at that address using malloc() as follows:

int *sum = (int *)malloc(sizeof(int));

Note: The header file stdlib.h needs to be included in order to use the malloc() function.

Next, we perform the addition by accessing the elements num1 and num2 from the argument structure, and store the results at the address pointed to by the integer pointer sum as follows:

*sum = params.num1 + params.num2;

Finally, we cast the integer pointer sum to a void pointer so that the pthread_exit() function can return the void pointer to the return value back to main().

Below is an extract of the main() function that waits for a return value from the thread function calcAdd.

// Declare a void pointer to the return value
void *valPtr;

// Spawn the calcAdd thread with 'params' as argument
pthread_t calcAdd_id;
pthread_create(&calcAdd_id, NULL, calcAdd, (void *)&params);

// Tell main() to wait for calcAdd to return a value
// The void pointer to the return value will be in valPtr
pthread_join(calcAdd_id, &valPtr);

// Cast the void pointer to an integer pointer
// and de-reference it to get actual value
printf("The sum is: %d \n", *(int *)valPtr);

Before spawning the thread, we have to declare a void pointer to the value that will be returned from the thread function calcAdd. Then, we create the thread normally using pthread_create() .

Next, pthread_join() is used to make main() wait until the thread function calcAdd returns. The void pointer valPtr now points to the return value of the thread function calcAdd.

Finally, in order to get the actual return value, we simply cast the void pointer valPtr back into an integer pointer using (int *) , and then de-reference the integer pointer using the * operator as follows:

*(int *)valPtr

The complete code is listed below:

/*
* Project: Pthreads Tutorial
* File: pthreads_return.c
* Date: February 19, 2022
* Author: M. Ray Auhammud
* License: MIT License
*/

#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>

void *calcAdd(void *arg);

typedef struct _args_calcAdd
{
    int num1;
    int num2;
} args_calcAdd;

int main()
{
    // Create an instance of the arguments structure
    args_calcAdd params;
    
    // Ask user to provide a number
    printf("Enter a number: ");
    scanf("%d", &params.num1);

    // Ask user to provide another number
    printf("Enter another number: ");
    scanf("%d", &params.num2);    
    
    // Declare a void pointer to the return value
    void *valPtr;

    // Spawn the calcAdd thread with 'params' as argument
    pthread_t calcAdd_id;
    pthread_create(&calcAdd_id, NULL, calcAdd, (void *)&params);

    // Tell main() to wait for calcAdd to return a value
    // The void pointer to the return value will be in valPtr
    pthread_join(calcAdd_id, &valPtr);

    // Cast the void pointer to an integer pointer
    // and de-reference it to get actual value
    printf("The sum is: %d \n", *(int *)valPtr);

    // This should be the last statement in main()
    pthread_exit(NULL);
}

void *calcAdd(void *arg)
{
    // Get the actual arguments structure
    args_calcAdd params = *(args_calcAdd *)arg;

    // Declare pointer to the return value
    // and allocate memory at that address
    int *sum = (int *)malloc(sizeof(int));

    // Perform the addition and store the results
    // at location pointed to by sum
    *sum = params.num1 + params.num2;

    // Return void pointer to the value we want to be returned
    pthread_exit((void *)sum);
}

Conclusion

The pthreads API makes it super easy to create and manage threads. We have seen how to spawn threads using the pthread_create() function, how to pass several parameters to a thread by bundling them into a structure, and how to be thread-safe.

Moreover, we learned how to return a value from a thread by

  1. using pthread_exit() to return a void pointer to the return value,

  2. and then calling pthread_join() in the main() function to get the void pointer to the return value,

  3. after which we can de-reference it to get the actual return value.

This concludes part 1 of this tutorial. I hope you enjoyed reading this article as much as I enjoyed writing it.

In part 2, we’re going to delve deeper into the pthreads API and have more fun with threads. Stay tuned :)

# Footnotes

https://www.cs.unibo.it/~ghini/didattica/sistop/pthreads_tutorial/POSIX_Threads_Programming.htm#Joining

https://man7.org/linux/man-pages/man7/pthreads.7.html

https://man7.org/linux/man-pages/man3/pthread_create.3.html

https://www.cse.cuhk.edu.hk/~ericlo/teaching/os/lab/9-PThread/Pass.html

https://stackoverflow.com/questions/3844678/pthread-exit-vs-return