Making Threads, and Waiting for Them
Chapter Twenty-Seven
Syllabus topic Module 1, "Processes - Threads - Overview"; Computer Science Practical 3, Module 1, "Practice thread creation and basic thread lifecycle using standard libraries"
Pages 105 to 108 of 452
In one line
pthread_create starts a new flow of execution inside this process, and pthread_join waits for one to finish.
The life cycle, which is what the practical asks for
A thread has four states in its life and each has a call or an event that moves it on.
| State | Reached by | Left by |
|---|---|---|
| Created, or ready | pthread_create returns | the scheduler choosing it |
| Running | being chosen | blocking, being preempted, or finishing |
| Blocked | waiting for a lock, a condition, input or output | the thing it waited for |
| Terminated | returning from its function, or pthread_exit | being joined, which releases its record |
A terminated thread that nobody joins is the thread version of Chapter twenty one's zombie. Its stack and its record stay allocated until somebody joins it. A program that creates threads in a loop and never joins them runs out of memory, and the two ways to avoid that are to join every thread or to detach it, which tells the library that nobody will ever join it and it may be cleaned up the moment it ends.
The five calls
| Call | What it does |
|---|---|
pthread_create(&id, attr, function, argument) | start a thread running function(argument) |
pthread_join(id, &result) | wait for that thread, and collect what it returned |
pthread_exit(value) | end this thread with that value |
pthread_detach(id) | nobody will join this one; clean it up when it ends |
pthread_self() | which thread am I |
The function takes one void and returns one void . That is the whole interface, and it is why every real program passes a pointer to a structure when it needs more than one argument.
Compile with -pthread. Without it the program may link and then behave strangely, because -pthread sets a compiler flag as well as adding the library.
Ten threads, made and joined
#define _POSIX_C_SOURCE 200809L
#include <stdio.h>
#include <pthread.h>
#define HOW_MANY 10
struct job {
int number;
long answer;
};
static void *work(void *argument)
{
struct job *j = argument;
j->answer = (long)j->number * j->number;
printf("thread %d computed %ld\n", j->number, j->answer);
return NULL;
}
int main(void)
{
pthread_t id[HOW_MANY];
struct job jobs[HOW_MANY];
for (int i = 0; i < HOW_MANY; i++) {
jobs[i].number = i + 1;
if (pthread_create(&id[i], NULL, work, &jobs[i]) != 0) {
perror("pthread_create");
return 1;
}
}
long total = 0;
for (int i = 0; i < HOW_MANY; i++) {
pthread_join(id[i], NULL); /* wait, in order */
total += jobs[i].answer;
}
printf("every thread joined, and the total is %ld\n", total);
return 0;
}$ gcc -std=c17 -Wall -Wextra -pthread -o ten ten.c
$ ./ten | grep thread | sort | tail -2
thread 8 computed 64
thread 9 computed 81
$ ./ten | grep total
every thread joined, and the total is 385
$ ./ten | grep total
every thread joined, and the total is 385Making Threads, and Waiting for Them
The total is the same every run and the order of the lines is not. The sum of the squares from one to ten is 385 whatever order the threads ran in, because each thread wrote to its own job. That is what makes this program correct, and Chapter thirty four is the same program written so that it is not.
The order really does change
$ for i in $(seq 20); do ./ten | head -1; done | sort -u > firsts.txt
$ n=$(wc -l < firsts.txt)
$ [ "$n" -gt 1 ] && echo "over twenty runs, $n different threads reached the screen first"
over twenty runs, 3 different threads reached the screen firstThree runs of the same program, and the thread that reached the screen first was not always the same. Nothing in the program decides that. It is the scheduler, and a program that depends on which thread runs first is wrong even when it works.
Counting the threads from outside
The kernel can be asked how many threads a process has while it is running.
#define _POSIX_C_SOURCE 200809L
#include <stdio.h>
#include <string.h>
#include <pthread.h>
#include <unistd.h>
static void *wait_a_while(void *unused)
{
(void)unused;
sleep(2);
return NULL;
}
int main(void)
{
pthread_t id[4];
for (int i = 0; i < 4; i++) {
pthread_create(&id[i], NULL, wait_a_while, NULL);
}
FILE *f = fopen("/proc/self/status", "r");
char line[128];
while (fgets(line, sizeof line, f)) {
if (strncmp(line, "Threads:", 8) == 0) {
printf("%s", line);
}
}
fclose(f);
for (int i = 0; i < 4; i++) {
pthread_join(id[i], NULL);
}
return 0;
}$ gcc -std=c17 -Wall -Wextra -pthread -o count-threads count-threads.c
$ ./count-threads
Threads: 5Five: the four that were created and the one that created them. The first thread of a process is a thread like any other, and main runs on it. There is no such thing as a process with no threads.
Passing an argument, and the trap in it
Passing the address of a loop variable to every thread is the commonest thread bug there is.
#define _POSIX_C_SOURCE 200809L
#include <stdio.h>
#include <pthread.h>
static void *show(void *argument)
{
int *n = argument;
printf("%d ", *n);
return NULL;
}
int main(void)
{
pthread_t id[4];
for (int i = 0; i < 4; i++) {
pthread_create(&id[i], NULL, show, &i); /* the SAME address, four times */
}
for (int i = 0; i < 4; i++) {
pthread_join(id[i], NULL);
}
printf("\n");
return 0;
}$ gcc -std=c17 -Wall -Wextra -pthread -o wrong wrong.c
$ ./wrong
4 4 4 4Four threads, and every one of them printed the same number. They were all given the address of i, which by the time they ran had reached 4. The fix is what ten.c did: give each thread a pointer to something of its own.
Making Threads, and Waiting for Them
Distinctions that carry marks
fork | pthread_create | |
|---|---|---|
| Creates | a process with its own address space | a thread sharing this one |
| The new one starts | at the return from fork | at the function given to it |
| Returns | twice | once |
| Cost | high | low |
| Collected with | wait | pthread_join |
| Uncollected leaves | a zombie process | a thread record and its stack |
pthread_join | pthread_detach | |
|---|---|---|
| Waits | yes | no |
| Gets the return value | yes | no |
| After it | the record is released | the record is released when the thread ends |
| Use when | you need the answer or the ordering | you do not care when it finishes |
What it does not mean
A thread function is not main. It takes one void and returns one void , and returning from it ends only that thread.
pthread_exit in main does not end the process. It ends the first thread and lets the others run on, which is occasionally what you want and usually a surprise.
Joining is not synchronisation between running threads. It waits for one to finish. Two threads that must co-ordinate while both are running need Chapters thirty seven to forty two.
Quick revision
pthread_create(&id, attr, function, argument)starts a thread;pthread_join(id, &result)
waits for it and collects its value.
- The function takes one
voidand returns onevoid. Pass a structure for more. - Compile with
-pthread. - Life cycle: created, running, blocked, terminated. A terminated thread's record survives until
it is joined or it was detached: the thread version of a zombie.
- The first thread of a process runs
main, so a process with four created threads reports five. - The order in which threads run is the scheduler's business and changes between runs. A
program that depends on it is wrong.
- Never pass the address of the loop variable to every thread: they all see its final value.
Test yourself
- Which calls create a thread and wait for one?
pthread_createandpthread_join. - What is the signature of a thread function? It takes one
void *and returns one
void *.
- What happens to a thread nobody joins? Its record and stack stay allocated, like a zombie
process. Join it, or detach it so the library cleans it up when it ends.
- A process creates four threads. How many does the kernel report and why? Five. The thread
running main is a thread like any other. 5. Four threads are given &i, the address of the loop variable, and all print 4. Why, and what is the fix? They share one variable, which had reached its final value before they ran. Give each thread a pointer to its own data.
Making Threads, and Waiting for Them
- Why does the total in
ten.cnever change while the order of the lines does? Each thread
wrote to its own structure, so nothing was shared and no result depended on the order. Only the moment each thread reached the screen depended on the scheduler.
The rest of this subject
These notes are cut from the University's printed syllabus. Open the syllabus itself, or the past papers, for the same subject.