POSIX threads (pthreads) are a standardized interface on operating system threads.
Relevant headers aside (they are discussed below), a program wishing to use pthreads must link against the pthreads library. Here is an example invocation of gcc demonstrating this:
gcc -pedantic -Wall -o theaded_program src.c -lpthread
The -l flag specifies the name of a library to link against (pthread, in our case); since pthreads is a system library, gcc knows where to find it.
Any program using pthreads will need to include pthread.h. Below is the Hello World of pthreads programs:
#include <pthread.h> #include <stdio.h> void * entry_point(void *arg) { printf("Hello world!\n"); return NULL; } int main(int argc, char **argv) { pthread_t thr; if(pthread_create(&thr, NULL, &entry_point, NULL)) { printf("Could not create thread\n"); return -1; } if(pthread_join(thr, NULL)) { printf("Could not join thread\n"); return -1; } return 0; }
There are two functions of note here:
Another point worth noting is that the thread entry point takes a single argument; the last argument to pthread_create is the argument that is passed to the entry point.
Similarly, threads can return a value to the function that calls pthread_join on them. See the man page for details.
In the above example, entry_point returns a NULL pointer to the thread that calls pthread_join on it. Sometimes, it may be desirable to exit a thread from some function other than the entry point.
#include <pthread.h> #include <stdio.h> void other_function() { pthread_exit(NULL); } void * entry_point(void *arg) { printf("Hello world!\n"); other_function(); printf("Hello again?\n"); return NULL; } int main(int argc, char **argv) { pthread_t thr; if(pthread_create(&thr, NULL, &entry_point, NULL)) { printf("Could not create thread\n"); return -1; } if(pthread_join(thr, NULL)) { printf("Could not join thread\n"); return -1; } return 0; }
In this example, note that "Hello again?" is never printed. The thread exits in other_function and entry_point never returns. The argument to pthread_exit is a value to be returned to the joining thread.
The pthreads specification provides many synchronization primitives; we will cover three in this primer:
Some parallel computations need to "meet up" at certain points before continuing. This can, of course, be accomplished with semaphores, but another construct is often more convenient: the barrier (the pthreads library pthread_barrier_t). As a motivating example, take this program:
#define _XOPEN_SOURCE 600 #include <pthread.h> #include <stdlib.h> #include <stdio.h> #define ROWS 10000 #define COLS 10000 #define THREADS 10 double initial_matrix[ROWS][COLS]; double final_matrix[ROWS][COLS]; // Barrier variable pthread_barrier_t barr; extern void DotProduct(int row, int col, double source[ROWS][COLS], double destination[ROWS][COLS]); extern double determinant(double matrix[ROWS][COLS]); void * entry_point(void *arg) { int rank = (int)arg; for(int row = rank * ROWS / THREADS; row < (rank + 1) * THREADS; ++row) for(int col = 0; col < COLS; ++col) DotProduct(row, col, initial_matrix, final_matrix); // Synchronization point int rc = pthread_barrier_wait(&barr); if(rc != 0 && rc != PTHREAD_BARRIER_SERIAL_THREAD) { printf("Could not wait on barrier\n"); exit(-1); } for(int row = rank * ROWS / THREADS; row < (rank + 1) * THREADS; ++row) for(int col = 0; col < COLS; ++col) DotProduct(row, col, final_matrix, initial_matrix); } int main(int argc, char **argv) { pthread_t thr[THREADS]; // Barrier initialization if(pthread_barrier_init(&barr, NULL, THREADS)) { printf("Could not create a barrier\n"); return -1; } for(int i = 0; i < THREADS; ++i) { if(pthread_create(&thr[i], NULL, &entry_point, (void*)i)) { printf("Could not create thread %d\n", i); return -1; } } for(int i = 0; i < THREADS; ++i) { if(pthread_join(thr[i], NULL)) { printf("Could not join thread %d\n", i); return -1; } } double det = Determinant(initial_matrix); printf("The determinant of M^4 = %f\n", det); return 0; }
This program spawns a number of threads, assigning each to compute part of a matrix multiplication. Each thread then uses the result of that computation in the next phase: another matrix multiplication.
There are a few things to note here:
The preprocessor definition of _XOPEN_SOURCE at the top of the program is important; without it, the barrier prototypes are not defined in pthread.h. The definition must come before any headers are included.
The pthreads library provides a basic synchronization primitive: pthread_mutex_t. The declarations required to use pthreads mutexes are included in pthread.h. This is a standard mutex with lock and unlock operations; see this example:
#include <pthread.h> #include <stdio.h> #include <math.h> #define ITERATIONS 10000 // A shared mutex pthread_mutex_t mutex; double target; void* opponent(void *arg) { for(int i = 0; i < ITERATIONS; ++i) { // Lock the mutex pthread_mutex_lock(&mutex); target -= target * 2 + tan(target); // Unlock the mutex pthread_mutex_unlock(&mutex); } return NULL; } int main(int argc, char **argv) { pthread_t other; target = 5.0; // Initialize the mutex if(pthread_mutex_init(&mutex, NULL)) { printf("Unable to initialize a mutex\n"); return -1; } if(pthread_create(&other, NULL, &opponent, NULL)) { printf("Unable to spawn thread\n"); return -1; } for(int i = 0; i < ITERATIONS; ++i) { pthread_mutex_lock(&mutex); target += target * 2 + tan(target); pthread_mutex_unlock(&mutex); } if(pthread_join(other, NULL)) { printf("Could not join thread\n"); return -1; } // Clean up the mutex pthread_mutex_destroy(&mutex); printf("Result: %f\n", target); return 0; }
The important functions for managing mutexes are:
The pthreads library itself does not provide a semaphore; however, a separate POSIX standard does define them. The necessary declarations to use these semaphores are contained in semaphore.h.
NOTE: Do not confuse these with SystemV semaphores which are in sys/sem.h.
#include <semaphore.h> #include <pthread.h> #include <stdio.h> #define THREADS 20 sem_t OKToBuyMilk; int milkAvailable; void* buyer(void *arg) { // P() sem_wait(&OKToBuyMilk); if(!milkAvailable) { // Buy some milk ++milkAvailable; } // V() sem_post(&OKToBuyMilk); return NULL; } int main(int argc, char **argv) { pthread_t threads[THREADS]; milkAvailable = 0; // Initialize the semaphore with a value of 1. // Note the second argument: passing zero denotes // that the semaphore is shared between threads (and // not processes). if(sem_init(&OKToBuyMilk, 0, 1)) { printf("Could not initialize a semaphore\n"); return -1; } for(int i = 0; i < THREADS; ++i) { if(pthread_create(&threads[i], NULL, &buyer, NULL)) { printf("Could not create thread %d\n", i); return -1; } } for(int i = 0; i < THREADS; ++i) { if(pthread_join(threads[i], NULL)) { printf("Could not join thread %d\n", i); return -1; } } sem_destroy(&OKToBuyMilk); // Make sure we don't have too much milk. printf("Total milk: %d\n", milkAvailable); return 0; }
The semaphore API has several functions of note:
Basic Management | Barriers | Mutexes | Semaphores | |
---|---|---|---|---|
Creation | pthread_create | pthread_barrier_init | pthread_mutex_init | sem_init |
Destroy | pthread_exit | pthread_barrier_destroy | pthread_mutex_destroy | sem_destroy |
Waiting | pthread_join | pthread_barrier_wait | - | - |
Acquisition | - | - | pthread_mutex_lock | sem_wait |
Release | - | - | pthread_mutex_unlock | sem_post |