Pthreads (POSIX Threads)
Pthreads (POSIX Threads)
At the dawn of parallel programming, hardware manufacturers implemented their own versions of threads, which significantly differed from one another, leading to difficulties in developing portable multi-threaded applications. For this reason, a standardized multi-thread programming interface was needed, and this was specified, for UNIX systems, by the IEEE POSIX 1003.1c standard in 1995. Consequently, thread implementations conforming to this standard are called POSIX threads, or Pthreads, and today, most hardware vendors provide Pthreads support.
From a programmer's perspective, Pthreads are defined as a set of types and functions for the C language, implemented in a header file named pthread.h and a library named pthread (although, in some Pthreads implementations, this library may be included in another library).
Implementing a Parallel Program Using Pthreads
Compilation and Execution
To compile a program that uses Pthreads, you will need to link the Pthreads library. Running the program is done just like any other C program. For example, if you are using the GCC compiler, you can compile and run a Pthreads program like this:
$ gcc -o program program.c -lpthread
./program
To implement a program that uses Pthreads, you need to include the pthread.h header.
Creating and Terminating Threads
In a C program with Pthreads, initially, there is a single thread of execution called the main thread. Any other thread must be explicitly created and started by the programmer, and this is done through the pthread_create function, which can be called as many times as needed and from anywhere in the code. The function has the following signature:
int pthread_create(pthread_t *thread, const pthread_attr_t *attr, void *(*start_routine) (void *), void *arg);
The thread
parameter (of type pthread_t
) represents an identifier for the new thread returned by this function, which can then be used to stop or reference the thread. The attr
parameter is used to set various attributes for the thread being created and can be set to NULL
if you want to use the default values. start_routine
is a pointer to the function that will be executed on the newly created thread when it starts. Finally, through the arg
parameter, you can pass a single parameter to the thread function, which must be passed by reference as a void
pointer (if you don't want to pass a parameter, you can leave it as NULL
). The function returns 0 if the new thread was successfully created and started, or an error code otherwise.
A thread created in this way can, in turn, create other threads, and there is no hierarchy or dependency between them. The maximum number of threads that can be created by a process depends on the Pthreads implementation, but generally, it is not recommended to have more threads than the number of CPU cores on the machine you are running on, due to overhead (this is a longer discussion that we recommend having with your assistant).
The function that a thread executes when you call pthread_create must have the following signature:
void *f(void *arg) {
[...]
return NULL;
/*
* Upon exiting the function, pthread_exit(NULL) will be called implicitly.
*/
}
The arg
parameter of the above function is received by the function f during its execution on a new thread when pthread_create is called, and it is equivalent to the arg
parameter of pthread_create. The limitation of being able to pass only one parameter to the thread function can be overcome by creating a struct with as many members as needed, which can then be passed as a reference. The pthread_exit function can be called to terminate the thread that calls it, and it takes either NULL
or a return value as a parameter, which will be passed further. Calling the pthread_exit function is not mandatory because it is implicitly called upon leaving a subroutine, and its parameter will be equal to the return value of the function.
When we call pthread_create, the newly created thread will run in parallel with the main thread. This means that all the code after the pthread_create function call will execute concurrently with the code of the new thread. If we want one thread to wait for the completion of another thread, we can achieve this using the pthread_join function. By calling this function, we can ensure that the calling thread blocks until the other thread finishes its processing (i.e., completes the thread function). The pthread_join function has the following signature:
int pthread_join(pthread_t thread, void **retval);
The thread
parameter represents the identifier of the thread we are waiting for, and retval
represents the return value of the expected thread function (and can be set to NULL
if we don't need this information). The function returns 0 in case of success or an error code otherwise.
A complete example of a C program using Pthreads can be seen below. In this example, two threads are created, both of which will run the same function f, but each of them will receive the current ID (0 or 1) as a parameter. After starting the two new threads, the main thread waits for both of them to finish, and then it exits.
#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>
#define NUM_THREADS 2
void *f(void *arg)
{
long id = *(long*) arg;
printf("Hello World from thread %ld!\n", id);
return NULL;
}
int main(int argc, char *argv[])
{
pthread_t threads[NUM_THREADS];
int r;
long id;
void *status;
long arguments[NUM_THREADS];
for (id = 0; id < NUM_THREADS; id++) {
arguments[id] = id;
r = pthread_create(&threads[id], NULL, f, (void *) &arguments[id]);
if (r) {
printf("Error when creating thread %ld\n", id);
exit(-1);
}
}
for (id = 0; id < NUM_THREADS; id++) {
r = pthread_join(threads[id], &status);
if (r) {
printf("Eroare when waiting for thread %ld\n", id);
exit(-1);
}
}
return 0;
}
A visual representation of the execution of the above program can be seen in the figure below.
In the above figure, it can be observed that initially, there is only one thread of execution, the main thread (marked as Tmain). When it calls pthread_create twice, it creates and starts two new threads (T0 and T1) that will run the function f with different parameters (0 and 1, respectively). At this point, we have three threads running in parallel. When the main thread calls pthread_join, it blocks until the two secondary threads finish their execution (i.e., until they finish running the function f). If one of the secondary threads has already completed its execution before the pthread_join call, the function will return instantly. At the end of the program, the main thread will also stop.