Threads in C — practice with a delay report, two tasks at once and a histogram with a mutex (and an & that got away from me)
The threads theory was fresh in my head and I jumped into my first program with an argument: a thread that received the threshold in minutes and counted the bus trips above it. I wrote it in one go, sure it was perfect, and the compiler stopped me dead:
int main() {
int threshold = 6;
pthread_t thread;
pthread_create(&thread, NULL, count, threshold); // the value 6 instead of its address
pthread_join(thread, NULL);
return 0;
}
Compiling it with gcc -Wall -o no_ampersand no_ampersand.c -lpthread:
no_ampersand.c: In function 'main': no_ampersand.c:21: error: passing argument 4 of 'pthread_create' makes pointer from integer without a cast [-Wint-conversion] In file included from no_ampersand.c:2: /usr/include/pthread.h:205: note: expected 'void * restrict' but argument is of type 'int'
I’d given pthread_create the value 6 where it expected an address. One single character was missing: &threshold. Older versions of gcc only warned, and when you ran it the program would normally crash (the thread tried to read at “address” 6); current ones, like Fedora’s, simply don’t compile it. The lesson I kept, and you’ll see it in today’s three programs: everything that goes into a thread is an address, and the thread has to know what type it is.
In the threads in C theory you saw pthread_create, pthread_join, void*, passing arguments and the mutex separately. Here we put them together in three medium-level programs with the minutes my bus was late: a report in a thread with a typed threshold (like exercise 4 of the exam), two threads with two different tasks on the same week and four threads filling a shared histogram with their mutex. The data look like real data: there are on-time trips, a 0, ties right on the limit and a line that is always late.
Table of Contents
Before you start
It helps to be clear on:
- The template
pthread_t h;→pthread_create(&h, NULL, f, arg);→pthread_join(h, NULL);and the signaturevoid* f(void* arg)withreturn NULL;. - How to get the argument back:
*(int*)argfor anint,(int*)argfor an array and(struct X*)argfor a struct. - That threads share globals and that, if two of them write to the same one, you need a mutex.
- Structs and the arrow operator
->(Structs and strings in C).
If any of these sounds shaky, review the theory before going on.
Always compile with gcc -Wall -o program program.c -lpthread and run with ./program. In the IC2 exam you do it on Fedora with gedit and the terminal; if you type gedit program.c &, the terminal doesn’t stay blocked while you edit.
Program 1: delay report with a typed threshold
The program asks from how many minutes a trip counts as late. Then it creates one thread that receives that threshold, walks through the week’s 8 trips and prints the ones above it, how many there are and which was the worst. What makes it tricky: the threshold lives in main and the thread has to read it through its address, and you have to decide what happens if the user types something that isn’t a number.
The idea
mainreads the threshold withscanfbefore creating the thread (if you created it first, the thread could read the variable before it has a value).mainpasses&thresholdtopthread_createand waits atpthread_join.- The thread copies the threshold into a local variable (
*(int*)arg) and walks the array once doing two things: counting the ones above the threshold and remembering the position of the worst.
Before programming, think about the worst trip: do you store its value or its position? If you store the position, you get both (delays[worst] is the value and worst + 1 the trip number).
The code
#include <stdio.h>
#include <pthread.h>
#define N 8
int delays[N] = {4, 0, 11, 7, 2, 15, 6, 9}; // minutes of delay of the week's 8 trips
// The thread receives the threshold as its argument and writes the whole report
void* report(void* arg) {
int threshold = *(int*)arg; // local copy: from here on it doesn't depend on main
int n = 0;
int worst = 0; // position of the most delayed trip
printf("Trips with more than %d minutes:", threshold);
for (int i = 0; i < N; i++) {
if (delays[i] > threshold) {
printf(" %d", delays[i]);
n++;
}
if (delays[i] > delays[worst]) { // > and not >=: on a tie the first one stays
worst = i;
}
}
printf("\n");
printf("There are %d trips with more than %d minutes of delay\n", n, threshold);
printf("The worst was trip %d, with %d minutes\n", worst + 1, delays[worst]);
return NULL;
}
int main() {
int threshold;
pthread_t thread;
printf("From how many minutes does it count as a delay? ");
if (scanf("%d", &threshold) != 1) { // if they don't type a number, we stop
printf("That's not a number\n");
return 1;
}
// threshold has a value BEFORE the thread is created: the thread reads it right away
if (pthread_create(&thread, NULL, report, &threshold) != 0) {
printf("Could not create the thread\n");
return 1;
}
pthread_join(thread, NULL);
printf("main: report finished\n");
return 0;
}
Output
In the run I typed a 6:
From how many minutes does it count as a delay? 6 Trips with more than 6 minutes: 11 7 15 9 There are 4 trips with more than 6 minutes of delay The worst was trip 6, with 15 minutes main: report finished
Checking by hand with 4, 0, 11, 7, 2, 15, 6 and 9: the ones greater than 6 are 11, 7, 15 and 9, four trips. The 6 of trip 7 does not count, because the condition is > and 6 isn’t greater than 6. The worst is the 15, the sixth trip.
The thread’s walk, iteration by iteration (the printf in the “Prints” column keeps adding numbers to the same line):
i | delays[i] | > 6? | n | worst (position) | Prints |
|---|---|---|---|---|---|
| 0 | 4 | No | 0 | 0 | — |
| 1 | 0 | No | 0 | 0 | — |
| 2 | 11 | Yes | 1 | 2 | 11 |
| 3 | 7 | Yes | 2 | 2 | 7 |
| 4 | 2 | No | 2 | 2 | — |
| 5 | 15 | Yes | 3 | 5 | 15 |
| 6 | 6 | No | 3 | 5 | — |
| 7 | 9 | Yes | 4 | 5 | 9 |
The important part of the thread, up close:
void* report(void* arg) {
int threshold = *(int*)arg; // local copy: from here on it doesn't depend on main
int n = 0;
int worst = 0; // position of the most delayed trip
printf("Trips with more than %d minutes:", threshold);
for (int i = 0; i < N; i++) {
if (delays[i] > threshold) {
printf(" %d", delays[i]);
n++;
}
if (delays[i] > delays[worst]) { // > and not >=: on a tie the first one stays
worst = i;
}
}
int threshold = *(int*)arg;makes a copy. From there on the thread no longer depends onmain‘s variable.worststarts at 0 (the first trip) and only changes with>: if there were two trips with 15, the first would stay.- The two
ifstatements go one after the other, separately, not withelse: a trip can be above the threshold and also be the worst.
Questions you might ask
Why does scanf come before pthread_create? Because the thread reads the threshold as soon as it starts. If you created it first, it could read threshold before it has a value, and count with any random number.
Why do I check what scanf returns? scanf returns how many values it read. If the user types “six”, it returns 0 and threshold is left without a value: better to warn and leave with return 1 than to build a report on garbage.
And what pthread_create returns? It returns 0 if it created the thread. It’s very unlikely to fail in a program like this, but checking costs one line and looks good in the exam.
Could main print the report instead of the thread? Yes, if the thread leaves n and worst in variables main can see (globals or a struct) and main prints after the join. You’ll do it that way in program 2. Here the thread prints because that’s what exercise 4 of the exam asks for: “the thread prints…”.
Why can threshold live in main instead of being global? Because main doesn’t leave its function while the thread works: it’s stopped at the join, and its variable still exists. If main finished first, the address would no longer be valid.
Passing threshold instead of &threshold (this article’s hook). With a modern gcc it doesn’t compile; with an old one it compiles with a warning and the program crashes when it runs.
Try it yourself
- Also count the trips that don’t go above the threshold and check that both counts add up to 8.
- Instead of the threshold, pass the thread a struct with the threshold and the trip number to start looking from.
Program 2: two threads, two tasks on the same week
Now we want two things from the same week: the statistics (mean, worst trip and its position) and a summary by category: on time (0-2 minutes), minor (3-5) and serious (6 or more). They’re two independent tasks, so each goes in its own thread and they work at the same time. What makes it tricky: each thread needs the array and its size, and has to return several results.
The idea
One struct per task with what it needs (the array and its size) and slots for what it returns. Both structs point to main‘s same array: reading it at the same time is not a problem. Each thread writes only to its own struct, so no mutex is needed. main waits for both and then prints, in the order it wants.
Before programming, think about what sets this program apart from the struct in the theory: there both threads did the same thing with different halves of the array; here they do different things with the whole array.
The code
#include <stdio.h>
#include <pthread.h>
#define N 8
// What the two tasks share (the data) and what each one returns
struct Statistics {
int* v; // the array of delays
int n; // how many trips
double mean; // result
int max; // result
int pos_max; // result: position of the maximum
};
struct Categories {
int* v;
int n;
int on_time; // 0-2 minutes
int minor; // 3-5 minutes
int serious; // 6 or more
};
void* statistics(void* arg) {
struct Statistics* e = (struct Statistics*)arg;
int sum = 0;
e->max = e->v[0];
e->pos_max = 0;
for (int i = 0; i < e->n; i++) {
sum += e->v[i];
if (e->v[i] > e->max) {
e->max = e->v[i];
e->pos_max = i;
}
}
e->mean = (double)sum / e->n; // the (double) avoids integer division
return NULL;
}
void* categories(void* arg) {
struct Categories* c = (struct Categories*)arg;
c->on_time = c->minor = c->serious = 0;
for (int i = 0; i < c->n; i++) {
if (c->v[i] <= 2) {
c->on_time++;
} else if (c->v[i] <= 5) { // here we already know it's greater than 2
c->minor++;
} else {
c->serious++;
}
}
return NULL;
}
int main() {
int delays[N] = {4, 0, 11, 7, 2, 15, 6, 9};
struct Statistics e = {delays, N, 0, 0, 0}; // both structs point to the SAME array
struct Categories c = {delays, N, 0, 0, 0};
pthread_t h1, h2;
pthread_create(&h1, NULL, statistics, &e); // two different tasks at the same time
pthread_create(&h2, NULL, categories, &c);
pthread_join(h1, NULL);
pthread_join(h2, NULL);
// main prints in the order it wants: the threads only compute
printf("Mean: %.2f minutes per trip\n", e.mean);
printf("Worst trip: number %d, with %d minutes\n", e.pos_max + 1, e.max);
printf("On time: %d · Minor: %d · Serious: %d\n", c.on_time, c.minor, c.serious);
return 0;
}
Output
Mean: 6.75 minutes per trip Worst trip: number 6, with 15 minutes On time: 2 · Minor: 1 · Serious: 5
Checking by hand: 4 + 0 + 11 + 7 + 2 + 15 + 6 + 9 = 54 and 54 / 8 = 6.75. The maximum is 15, at position 5 (trip 6). Categories: on time the 0 and the 2 (2), minor the 4 (1) and serious 11, 7, 15, 6 and 9 (5); 2 + 1 + 5 = 8 trips. This output always comes out in the same order, even if the threads finish in any order, because main is the one printing.
The two functions up close:
void* statistics(void* arg) {
struct Statistics* e = (struct Statistics*)arg;
int sum = 0;
e->max = e->v[0];
e->pos_max = 0;
for (int i = 0; i < e->n; i++) {
sum += e->v[i];
if (e->v[i] > e->max) {
e->max = e->v[i];
e->pos_max = i;
}
}
e->mean = (double)sum / e->n; // the (double) avoids integer division
return NULL;
}
void* categories(void* arg) {
struct Categories* c = (struct Categories*)arg;
c->on_time = c->minor = c->serious = 0;
for (int i = 0; i < c->n; i++) {
if (c->v[i] <= 2) {
c->on_time++;
} else if (c->v[i] <= 5) { // here we already know it's greater than 2
c->minor++;
} else {
c->serious++;
}
}
return NULL;
}
The categories one, iteration by iteration:
i | v[i] | Branch | on_time | minor | serious |
|---|---|---|---|---|---|
| 0 | 4 | <= 5 | 0 | 1 | 0 |
| 1 | 0 | <= 2 | 1 | 1 | 0 |
| 2 | 11 | else | 1 | 1 | 1 |
| 3 | 7 | else | 1 | 1 | 2 |
| 4 | 2 | <= 2 | 2 | 1 | 2 |
| 5 | 15 | else | 2 | 1 | 3 |
| 6 | 6 | else | 2 | 1 | 4 |
| 7 | 9 | else | 2 | 1 | 5 |
Questions you might ask
Why two different structs and not just one? Because each task returns different things. Also, if both threads wrote to the same struct you’d have to think about mutexes; with one each, they can’t overwrite each other.
Isn’t it dangerous for both to read the same array at once? No. The problem appears when someone writes. Reading at the same time is safe: nobody changes the data.
Why else if (c->v[i] <= 5) and not >= 3 && <= 5? Because if we reach that else if we already know the value is greater than 2 (otherwise it would have gone into the first if). The if/else chain keeps ruling things out.
What does c->on_time = c->minor = c->serious = 0; do? It sets all three to 0 in one line: assignment is evaluated right to left and returns the assigned value.
Why (double)sum / e->n? Without the conversion, 54 / 8 would be integer division and give 6. With (double) in front, the division is real: 6.75.
Printing e.mean before the pthread_join calls. You’d get 0.00 (the value the struct started with), because the thread hasn’t computed it yet.
When the threads only compute and main prints at the end, the output is always the same between runs. It’s the easiest way to match the statement’s output.
Try it yourself
- Add a third task in another thread: reverse the order of the trips into a second array (not the same one, which the other two are reading).
- Make the category limits fields of the struct and try with on time up to 1 minute.
Program 3: four lines, one shared histogram
Four bus lines, seven days each. We want a histogram of all the trips (how many on time, minor and serious) and the total minutes lost across all of them. Each line is handled by a thread, and all four add to the same variables. What makes it tricky: here several threads do write to the same thing, so a mutex is needed.
The idea
- One
Linestruct per thread with its name and its 7 delays (each thread receives&lines[k]). - The histogram (
int histogram[3]), the total and the lock are global: all four threads must use the same ones. - Each thread, for each day, works out the category outside the lock (it touches nothing shared) and only closes the lock to add.
maincreates the four in one loop, waits for them in another loop and prints.
Before programming, think about why main needs two loops. If you did create and join in the same loop, each thread would finish before the next one was created and they wouldn’t work at the same time.
The code
#include <stdio.h>
#include <pthread.h>
#define DAYS 7
#define LINES 4
// What each thread receives: the name of its line and its 7 delays
struct Line {
char* name;
int v[DAYS];
};
int histogram[3] = {0, 0, 0}; // shared: [0] on time, [1] minor, [2] serious
int total_minutes = 0; // shared
pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER;
void* classify(void* arg) {
struct Line* l = (struct Line*)arg;
for (int d = 0; d < DAYS; d++) {
int cat; // the category is worked out OUTSIDE the lock: it touches nothing shared
if (l->v[d] <= 2) {
cat = 0;
} else if (l->v[d] <= 5) {
cat = 1;
} else {
cat = 2;
}
pthread_mutex_lock(&lock); // from here on, one thread at a time
histogram[cat]++;
total_minutes += l->v[d];
pthread_mutex_unlock(&lock);
}
return NULL;
}
int main() {
struct Line lines[LINES] = {
{"Line 1", {3, 5, 2, 8, 4, 6, 1}},
{"Line 12", {9, 12, 7, 10, 14, 8, 11}},
{"Line 17", {0, 2, 1, 3, 0, 4, 2}},
{"Line 91", {6, 4, 9, 5, 7, 3, 8}},
};
pthread_t h[LINES];
for (int k = 0; k < LINES; k++) {
pthread_create(&h[k], NULL, classify, &lines[k]); // four threads, one per line
}
for (int k = 0; k < LINES; k++) {
pthread_join(h[k], NULL);
}
printf("Trips of the week on the %d lines: %d\n", LINES, LINES * DAYS);
printf("On time (0-2 min): %d\n", histogram[0]);
printf("Minor (3-5 min): %d\n", histogram[1]);
printf("Serious (6+ min): %d\n", histogram[2]);
printf("Minutes lost: %d\n", total_minutes);
return 0;
}
Output
Trips of the week on the 4 lines: 28 On time (0-2 min): 7 Minor (3-5 min): 8 Serious (6+ min): 13 Minutes lost: 154
Line-by-line check:
| Line | Delays | On time (0-2) | Minor (3-5) | Serious (6+) | Minutes |
|---|---|---|---|---|---|
| 1 | 3 5 2 8 4 6 1 | 2 | 3 | 2 | 29 |
| 12 | 9 12 7 10 14 8 11 | 0 | 0 | 7 | 71 |
| 17 | 0 2 1 3 0 4 2 | 5 | 2 | 0 | 12 |
| 91 | 6 4 9 5 7 3 8 | 0 | 3 | 4 | 42 |
| Total | 28 trips | 7 | 8 | 13 | 154 |
7 + 8 + 13 = 28 trips, and 29 + 71 + 12 + 42 = 154 minutes. Line 12 didn’t make a single on-time trip all week.
The thread up close:
void* classify(void* arg) {
struct Line* l = (struct Line*)arg;
for (int d = 0; d < DAYS; d++) {
int cat; // the category is worked out OUTSIDE the lock: it touches nothing shared
if (l->v[d] <= 2) {
cat = 0;
} else if (l->v[d] <= 5) {
cat = 1;
} else {
cat = 2;
}
pthread_mutex_lock(&lock); // from here on, one thread at a time
histogram[cat]++;
total_minutes += l->v[d];
pthread_mutex_unlock(&lock);
}
return NULL;
}
int cat;and theifstatements go before thelock: working out the category only looks atl->v[d], which belongs to that thread’s struct. Nobody else touches it, so there’s nothing to protect.- Between
lockandunlockonly two lines are left: the ones writing to shared data. The shorter the critical section, the less the threads wait for each other. pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER;is global for the same reason as the histogram: if each thread had its own lock, it would protect nothing.
Questions you might ask
Is the mutex really needed with so little data? With 28 additions you normally won’t see the race, because each thread finishes very quickly. But the bug is there: in the theory you saw almost half a million increments vanish when the threads did more work. The program has to be correct always, not only when you’re lucky.
Wouldn’t it be faster for each thread to build its own histogram and main to add them up at the end? Yes, and it’s a very good solution: each thread writes to its own struct (no mutex) and main adds after the join calls. Here we use the shared one to practice the mutex; in the exercises you’ll see the middle version: each thread computes locally and enters the lock just once.
Why does {"Line 1", {3, 5, 2, 8, 4, 6, 1}} work? It’s the initialisation of an array of structs: each outer brace is a struct, with its string and its array of 7 integers.
In what order do the threads add? Any order, and it changes between runs. It doesn’t matter: the sum doesn’t depend on the order, and the mutex guarantees none is lost.
Declaring the pthread_mutex_t inside the thread function. Each thread would have its own lock, always open for it, and the race would still be there.
Try it yourself
- Remove the
lockand theunlock, multiply the days by a lot (for example, repeat each line 100,000 times with an outer loop) and see whether the total stops adding up. - Add a
minutesfield to theLinestruct for each thread to fill in, and havemainprint which line lost the most time.
Common mistakes in this practice
- Passing the value instead of the address (
thresholdinstead of&threshold): it doesn’t compile with a modern gcc. - Creating the thread before having the data (
scanfafterpthread_create): the thread reads a variable with no value. - Using the results before the
join: you get the struct’s initial values (0 or 0.00). - The same struct for two threads that write results: they overwrite each other.
- The mutex declared inside the thread, or the whole loop inside the critical section: either it protects nothing or the threads stop working at the same time.
createandjoinin the same loop: the program works, but the threads run one after another.
Watch it step by step in Python Tutor
Python Tutor draws the variables and memory while running the code line by line (choose C). It doesn’t run threads, so this program calls the statistics task from program 2 as a normal function: the void* conversion and the struct are exactly the same.
#include <stdio.h>
// Python Tutor doesn't run threads: we call the task like a normal function
struct Statistics {
int* v;
int n;
int max;
int pos_max;
};
void* statistics(void* arg) {
struct Statistics* e = (struct Statistics*)arg; // the same conversion as in the thread
e->max = e->v[0];
e->pos_max = 0;
for (int i = 1; i < e->n; i++) {
if (e->v[i] > e->max) {
e->max = e->v[i];
e->pos_max = i;
}
}
return NULL;
}
int main() {
int delays[8] = {4, 0, 11, 7, 2, 15, 6, 9};
struct Statistics e = {delays, 8, 0, 0};
statistics(&e); // in the real program: pthread_create(&h1, NULL, statistics, &e)
printf("Worst trip: number %d, with %d minutes\n", e.pos_max + 1, e.max);
return 0;
}
Its output, run with gcc:
Worst trip: number 6, with 15 minutes
What to watch step by step:
- When
eis created, notice that its fieldvis an arrow tomain‘sdelaysarray: the struct doesn’t copy the data, it points to them. - When entering
statistics,argand the innerepoint to the same struct inmain. That’s the(struct Statistics*)argconversion. - Watch
e->pos_maxchange ati = 2(the 11) and ati = 5(the 15), and never again. - Back in
main,e.maxande.pos_maxalready hold 15 and 5: the result stayed in the struct, with noreturn.
Summary
- A thread receives a single argument: an address. For a number,
&x; for several values, one struct per thread. - The data the thread needs must have a value before
pthread_create. - If threads only compute and
mainprints after thejoincalls, the output is always the same. - Reading the same data at once is safe; writing to the same thing at once is not.
- The mutex is global and only protects the lines that touch shared data.
- Two loops in
main: one forcreateand another forjoin.
Checklist: have you practised it?
- Have you run the three programs and checked their output?
- Can you explain why the 6 doesn’t count with threshold 6?
- Can you say, without looking, which program needs a mutex and which doesn’t, between programs 2 and 3?
- Could you pass the threshold inside a struct instead of with
&threshold? - Have you tried the “Try it yourself” ideas without looking at the theory?
- Do you always compile with
-Walland-lpthread?
If you answered yes to almost everything, you’re ready for the threads in C exercises, where I don’t hand you the code anymore. See you there!
Next step: exercisesThreads in C — exercises with a backwards bus stop, struct queries and a month split across four threads (and four threads that claimed to be the same bus)Did this article help you? Here is how you can support SergioLearns →



