POSIX threads
A thread is a second flow of control inside one process. It shares the global variables and the heap with the others and has its own stack, so a function can run while main continues. The POSIX calls are pthread_create and pthread_join, and the program links with -pthread.
Creating and joining
pthread_create takes the thread handle, the attributes, the function and one argument. The function has the fixed signature void *(*)(void *) and returns a pointer that pthread_join can collect. The argument is a pointer, so a small integer is passed through a cast.
#include <pthread.h>
#include <stdio.h>
static void *worker(void *argument)
{
long id = (long) argument;
printf("thread %ld\n", id);
return NULL;
}
int main(void)
{
pthread_t first;
pthread_t second;
pthread_create(&first, NULL, worker, (void *) 1L);
pthread_create(&second, NULL, worker, (void *) 2L);
pthread_join(first, NULL);
pthread_join(second, NULL);
printf("main done\n");
return 0;
}
gcc -Wall -Wextra -pthread create.c -o create
./create
thread 1
thread 2
main done
The two threads may print in either order, and this run happened to keep the creation order. pthread_join waits for a thread and makes the program deterministic at the end: the last line appears only after both threads have finished. Without the joins, main could return and end the process first.
Passing data to a thread
One pointer is the only argument, so a struct carries everything the thread needs. The array of jobs lives in main and is still valid while the threads run, because the joins happen before the function returns. Each thread receives the address of one element, not the index it would have to look up.
#include <pthread.h>
#include <stdio.h>
struct Job {
int id;
int value;
};
static void *run(void *argument)
{
struct Job *job = argument;
printf("job %d: %d squared is %d\n", job->id, job->value, job->value * job->value);
return NULL;
}
int main(void)
{
struct Job jobs[2] = {{1, 3}, {2, 4}};
pthread_t threads[2];
for (int i = 0; i < 2; i++) {
pthread_create(&threads[i], NULL, run, &jobs[i]);
}
for (int i = 0; i < 2; i++) {
pthread_join(threads[i], NULL);
}
return 0;
}
gcc -Wall -Wextra -pthread args.c -o args
./args
job 1: 3 squared is 9
job 2: 4 squared is 16
The address of a local array is safe here because main does not return until both joins complete. A thread outliving the array is the classic bug: the address stays, the storage does not, and the read is undefined.
Detached threads
A joined thread keeps its resources until somebody joins it. A detached thread releases them when it exits, and nobody may join it. The example detaches the thread and gives it a moment to print, which is enough for a demonstration and not a synchronisation mechanism.
#include <pthread.h>
#include <stdio.h>
#include <unistd.h>
static void *background(void *argument)
{
(void) argument;
printf("background thread runs\n");
return NULL;
}
int main(void)
{
pthread_t thread;
pthread_create(&thread, NULL, background, NULL);
pthread_detach(thread);
printf("main waits a little\n");
usleep(50000);
printf("main done\n");
return 0;
}
gcc -Wall -Wextra -pthread detach.c -o detach
./detach
main waits a little
background thread runs
main done
The order of the first two lines is not guaranteed; the sleep only makes the background thread likely to finish first. A real program that needs the result waits for it, either with a join (not allowed after detach) or with a condition variable or a semaphore, the subject of lesson 73.
The wrong thread signature
pthread_create expects a function of exactly one pointer returning a pointer. A function with no parameters is a different type, and C23 rejects the call instead of letting the thread start with a garbage argument. The note prints both signatures.
#include <pthread.h>
#include <stdio.h>
static void worker(void)
{
printf("worker\n");
}
int main(void)
{
pthread_t thread;
pthread_create(&thread, NULL, worker, NULL);
pthread_join(thread, NULL);
return 0;
}
gcc -Wall -Wextra -pthread badsig.c -o badsig
badsig.c: In function 'main':
badsig.c:13:35: error: passing argument 3 of 'pthread_create' from incompatible pointer type [-Wincompatible-pointer-types]
13 | pthread_create(&thread, NULL, worker, NULL);
| ^~~~~~
| |
| void (*)(void)
In file included from badsig.c:1:
/usr/include/pthread.h:80:77: note: expected 'void * (*)(void *)' but argument is of type 'void (*)(void)'
The fix is the signature from the first section: a void * parameter, even when the thread needs no data, and a void * return, even when there is no result. Joining a detached thread is the other common mistake; the call is not checked by the compiler and the result depends on the thread's state, so the rule is the one from the detach section: a detached thread is never joined.
Exercises
- Create four threads that each print their id and join all of them; run the program several times and compare the order.
- Pass a pointer to a struct with two numbers and have the thread return a pointer to a static result; collect it with pthread_join.
- Remove -pthread from the command and see what the linker says on this system.
Questions
A new set of questions in a new order every time. Choose one answer for each, then check.
-
What does a thread share with the other threads of its process?
Answer
Global variables and the heap
Threads share the address space, so globals and heap blocks are visible to all, while each thread has its own stack.
-
What signature must a
pthread_createfunction have?Answer
void *(*)(void *)The thread function receives one pointer and returns a pointer, and a different signature is rejected by the compiler.
-
What is the difference between joining and detaching a thread?
Answer
A join collects the thread; a detach releases it
A joined thread waits to be collected, while a detached thread releases its resources when it exits and cannot be joined.
-
Why is
-pthreadadded to the build command?Answer
It enables the thread-safe compiler and linker settings
The flag defines the thread macros and links the POSIX thread library, which the calls need.
-
What does this program print after both joins?
#include <pthread.h> #include <stdio.h> static void *work(void *a){(void)a;return NULL;} int main(void){pthread_t a,b;pthread_create(&a,0,work,0);pthread_create(&b,0,work,0);pthread_join(a,0);pthread_join(b,0);printf("main done\n");return 0;}Answer
main done
Both threads are joined before the final line, so the program ends with
main done. -
What does this program print for each job?
#include <pthread.h> #include <stdio.h> struct Job{int id,value;}; static void *run(void *a){struct Job*j=a;printf("job %d: %d\n",j->id,j->value*j->value);return 0;} int main(void){struct Job jobs[2]={{1,3},{2,4}};pthread_t t[2];for(int i=0;i<2;i++)pthread_create(&t[i],0,run,&jobs[i]);for(int i=0;i<2;i++)pthread_join(t[i],0);return 0;}Answer
job 1: 9 / job 2: 16
Each thread receives one job and prints its value multiplied by itself.
-
What does the code do before printing
main done?Answer
It sleeps for 50 milliseconds
The main thread calls
usleepfor fifty milliseconds before it prints the final line, which gives the detached thread a chance to run. -
What does GCC report for a thread function with no parameters?
Answer
An incompatible pointer type at
pthread_createThe expected type is
void *(*)(void *), so avoid (*)(void)argument is a different pointer type and is rejected. -
What happens when a detached thread is joined?
Answer
The call is not checked and the result is undefined
The compiler cannot see the thread state, so the rule is to join a thread once and never after detaching it.
-
Why is passing the address of a local array to a thread safe here?
Answer
The joins finish before
mainreturnsThe storage lives until
mainreturns, and the joins keepmainalive until every thread has finished with it.
The code on this page was run with gcc (Alpine 15.2.0) 15.2.0 on Alpine Linux 3.24.3 (x86_64) in a Docker container.
Article author: Arthur Isaev