POSIX Parallel Programming, Part 3: Threads
- Workers of the World
- Lock the Door Behind You
- On One Condition
- Reading, Writing, and Arithmetic
- Loose Threads
So far in this series we’ve looked at spawning new processes and communicating among them. Processes are the traditional mechanism for parallelism on UNIX platforms. Recently, however, the POSIX threading APIs have gained widespread support. Unlike processes created using fork(2), threads spawned with pthread_create(3) exist in the same address space as their parent.
Older versions of Linux relied on a userspace implementation provided by glibc. This technique put all of a process’s threads in the same kernel-scheduled entity and used timer signals to switch between them. More recent versions use the clone(2) system call, which is similar to fork(2) but allows the child process to share the parent’s address space. Other UNIX-like systems have similar mechanisms, although some use a N:M kernel-scheduled entities-to-threads mapping. This enables threads that spend most of their time waiting for data to be multiplexed onto a single kernelspace entity, while allowing CPU-limited ones to be scheduled independently. On paper, this strategy has a number of advantages, although in practice it is harder to get right.
Workers of the World
The primary reason for creating a thread is to get some work done in the background. Since the main way of getting work done in a C program is to call a function, the pthread_create(3) call takes a function as an argument and runs that function in a separate thread.
The function passed to the pthread_create(3) call takes a pointer as an argument, and returns a pointer. This pointer can later be retrieved using the pthread_join(3) function. This setup allows you to implement futures quite easily; your parent thread calls a function in a new thread, does some other work, and then waits for the worker thread to finish.
Listing 1 contains a simple program for determining whether a number is prime.
Listing 1 primes.c.
#include <stdio.h> #include <stdlib.h> #include <pthread.h> #include <math.h> #define PRIME 0 #define NOT_PRIME 1 void * make_sieve(int* numbers) { char * sieve = calloc(*numbers, sizeof(char)); int test_max = sqrt(*numbers) + 1; //From the definition of prime numbers: sieve[0] = sieve[1] = NOT_PRIME; //Create the sieve for(int i=2 ; i<test_max ; i++) { //If the current number is prime, try dividing all //subsequent potential primes by it if(sieve[i] == PRIME) { //Any number which is a product of a prime number //is not prime itself for(int j=i+i ; j<*numbers ; j+=i) { sieve[j] = NOT_PRIME; } } } return sieve; } int main(void) { pthread_t thread; int max = 20000; int test_number; char* sieve; //Spawn a thread to create the sieve pthread_create(&thread, NULL, (void*(*)(void*))make_sieve, (void*)&max); printf("Enter a number: "); scanf("%d", &test_number); //Collect the sieve pthread_join(thread, (void**)&sieve); //Check that the entered number is in range if(test_number >= max) { test_number = max-1; } if(sieve[test_number] == PRIME) { printf("\n%d is prime.\n", test_number); } else { printf("\n%d is not prime.\n", test_number); } return 0; }
When you run this program, it spawns a worker thread that crease a Sieve of Eratosthenes—an array indicating whether a range of numbers is prime. This process happens in the background while the program asks the user to enter a number. Once the user has entered the number, the main thread waits for the worker thread to finish and then uses the result to see whether the entered number is prime.
Note that the signature of the make_sieve() function doesn’t match that expected by the pthread_create(3) function. Because both accept a pointer, however, we can cast it to the correct form and receive no errors.
The pthread_create(3) call used in this program looks like this:
pthread_create(&thread, NULL, (void*(*)(void*))make_sieve, (void*)&max);
- The first argument is a pointer to a pthread_t that’s set to an identifier for this thread. Future thread operations should use this identifier to identify the created thread.
- The second argument specifies some attributes for the thread. This can be an attribute set created with the pthread_attr_*(3) family of functions, or NULL for the default options.
- The third and fourth arguments are the function to start and the argument to pass to it, respectively. Notice that we put in an explicit cast here so that our function can receive an int* rather than a void*.