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

  1. Create four threads that each print their id and join all of them; run the program several times and compare the order.
  2. 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.
  3. 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.

  1. 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.

  2. What signature must a pthread_create function have?

    Answer

    void *(*)(void *)

    The thread function receives one pointer and returns a pointer, and a different signature is rejected by the compiler.

  3. 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.

  4. Why is -pthread added 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.

  5. 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.

  6. 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.

  7. What does the code do before printing main done?

    Answer

    It sleeps for 50 milliseconds

    The main thread calls usleep for fifty milliseconds before it prints the final line, which gives the detached thread a chance to run.

  8. What does GCC report for a thread function with no parameters?

    Answer

    An incompatible pointer type at pthread_create

    The expected type is void *(*)(void *), so a void (*)(void) argument is a different pointer type and is rejected.

  9. 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.

  10. Why is passing the address of a local array to a thread safe here?

    Answer

    The joins finish before main returns

    The storage lives until main returns, and the joins keep main alive 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