Threads 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)
I wanted four threads to introduce themselves, each with its bus line: 1, 12, 17 and 91. I wrote a loop with pthread_create and gave each thread &i, which seemed the most natural thing in the world. It compiled without a single warning, I ran it, and three of the four threads swore they were line 17. The fourth said it was line… 0, which doesn’t even exist. You’ll find the whole thing in snippet 2 of “What’s wrong here?”, with the explanation and the fix.
Here I don’t hand you the code anymore: three exercises of growing difficulty with the same bus delays as the theory and the practice, three snippets with bugs that show up in the exam and, at the end, the cheat sheet to have everything at hand on the day of exercise 4.
Table of Contents
How to use this article
- Read the statement and the expected output of each level, and write your program on your Linux (in the exam: Fedora, gedit and the terminal) without looking at the solutions.
- Compile it with
gcc -Wall -o program program.c -lpthreadand compare your output with the expected one. If you get stuck, open the hint. - Before reading the solutions, try to predict what each snippet in “What’s wrong here?” does.
- Compare your code with the solution and read “Why it’s done this way”: that’s where the decisions that earn marks are.
Level 1 (basic): the backwards bus stop
The San Telmo bus stop has a broken sign that shows its name backwards. Write a program with the global string char stop[] = "San Telmo"; that creates one thread that reverses it in the same memory (without using another string), walking it from both ends and swapping characters. main prints the string before creating the thread and, after waiting for it, prints it reversed.
Expected output:
Before: San Telmo After: omleT naS
Two indices, left = 0 and right = strlen(stop) - 1, that move closer while left < right. To swap two characters you need a temporary variable. And don’t forget #include <string.h> for strlen.
Level 2 (intermediate): two queries with a struct
With the second week’s delays, {3, 8, 0, 12, 5, 6, 1, 10}, and a threshold of 5 minutes, create two threads: one counts the trips with less than 5 minutes and the other the ones with more than 5. Each thread receives a struct with the array, its size, the threshold and a field for the result. main waits for both and prints both results and how many trips had exactly 5 minutes (without a third thread: it comes from a subtraction).
Expected output:
Trips with less than 5 minutes: 3 Trips with more than 5 minutes: 4 Trips with exactly 5 minutes: 1
Declare two struct variables, one for each thread, even though both point to the same array and have the same threshold. What would happen to result if both threads shared the same struct?
Level 3 (challenge): the month split across four threads
You have the delays for the 30 days of a month:
int month[30] = {4, 0, 11, 7, 2, 15, 6, 9, 3, 5, 1, 8, 12, 0, 2,
6, 10, 4, 7, 3, 0, 14, 5, 9, 1, 6, 2, 8, 11, 3};
Split the days across 4 threads as evenly as possible (30 isn’t divisible by 4: the leftover days, one more for the first threads). Each thread works out the sum and the maximum of its chunk and prints a line with them. On top of that, all of them must leave the month’s total and the worst day in two shared global variables, protected with a mutex, entering the critical section only once per thread. main prints the total, the mean per day and the worst day at the end.
Expected output (the threads’ lines may come out in another order; the last one, no):
Thread 1: days 1-8, 54 minutes, max 15 Thread 2: days 9-16, 37 minutes, max 12 Thread 4: days 24-30, 40 minutes, max 11 Thread 3: days 17-23, 43 minutes, max 14 Whole month: 174 minutes of delay, mean 5.80 per day, worst day 15 minutes
30 / 4 gives 7 and 30 % 4 gives 2: threads 1 and 2 take 8 days and threads 3 and 4, 7. Each thread adds up in local variables and only at the end closes the lock to add its share to the total and compare its maximum with the worst.
What’s wrong here?
Three snippets with typical bugs from the threads exercise. Try to predict what each one does before reading the explanation.
Snippet 1: the thread function
#include <stdio.h>
#include <pthread.h>
int delays[8] = {4, 0, 11, 7, 2, 15, 6, 9};
int n = 0;
void count() { // looks like a normal function...
for (int i = 0; i < 8; i++) {
if (delays[i] > 5) {
n++;
}
}
}
int main() {
pthread_t thread;
pthread_create(&thread, NULL, count, NULL);
pthread_join(thread, NULL);
printf("%d delayed trips\n", n);
return 0;
}
Does it compile? What does it print?
What happens: with gcc 14 or later (Fedora ships a newer version), it doesn’t even compile:
signature.c: In function 'main': signature.c:17: error: passing argument 3 of 'pthread_create' from incompatible pointer type [-Wincompatible-pointer-types] In file included from signature.c:2: /usr/include/pthread.h:204: note: expected 'void * (*)(void *)' but argument is of type 'void (*)()'
pthread_create expects a function of type void* (*)(void*), “takes a void* and returns a void*“, and count is void (*)(), “takes nothing and returns nothing”. Older versions of gcc let it through with a warning; new ones treat it as an error.
Writing the thread function like a normal function. In the exam, even if it compiled with a warning, the marker spots it straight away.
The fix is to change only the signature and add the return NULL;:
void* count(void* arg) { // the signature pthread_create asks for
for (int i = 0; i < 8; i++) {
if (delays[i] > 5) {
n++;
}
}
return NULL;
}
With that it compiles without warnings and the output is the expected one:
5 delayed trips
Always write the shell void* name(void* arg) { return NULL; } first and then fill in the body. That way you forget neither the signature nor the return.
Snippet 2: four threads in a loop
#include <stdio.h>
#include <pthread.h>
int lines[4] = {1, 12, 17, 91};
void* announce(void* arg) {
int k = *(int*)arg; // what value does i have when the thread gets here?
printf("Thread %d: I'm line %d\n", k, lines[k]);
return NULL;
}
int main() {
pthread_t h[4];
for (int i = 0; i < 4; i++) {
pthread_create(&h[i], NULL, announce, &i); // all four receive the same address
}
for (int i = 0; i < 4; i++) {
pthread_join(h[i], NULL);
}
return 0;
}
Which line does each thread claim to be?
What happens: this is the real output of my single run:
Thread 2: I'm line 17 Thread 2: I'm line 17 Thread 2: I'm line 17 Thread 4: I'm line 0
All four threads receive the same address, the address of i. They don’t receive the value i had when they were created, but a place to read it from when they start. And by the time a thread reaches *(int*)arg, the loop has moved on: here three of them read a 2 and the last one a 4, the value the loop ends with. With 4, lines[4] falls outside the array (its positions are 0-3) and reads whatever is in memory right after it: in my run, a 0. On top of that, i stops existing when the for ends, so the thread reads a variable that belongs to nobody anymore. On your computer you’ll get other numbers, and that’s the worst part: it may even come out right.
Passing &i (or the address of any variable the loop keeps changing) to several threads.
The fix is to give each thread its own slot, which nobody touches again:
pthread_t h[4];
int ids[4]; // one slot per thread: nobody touches it again
for (int i = 0; i < 4; i++) {
ids[i] = i;
pthread_create(&h[i], NULL, announce, &ids[i]);
}
for (int i = 0; i < 4; i++) {
pthread_join(h[i], NULL);
}
return 0;
Output of the fix:
Thread 0: I'm line 1 Thread 3: I'm line 91 Thread 1: I'm line 12 Thread 2: I'm line 17
Now each thread says its right line. The order of the lines still changes between runs (here it came out 0, 3, 1, 2), because the four work at the same time; if you need it fixed, have the threads store the value and let main print after the join calls.
If each thread needs more than one value, instead of int ids[4] use an array of structs, one per thread, as in level 3.
Snippet 3: the total too early
#include <stdio.h>
#include <pthread.h>
int delays[8] = {4, 0, 11, 7, 2, 15, 6, 9};
int total = 0;
void* add_up(void* arg) {
for (int i = 0; i < 8; i++) {
total += delays[i];
}
return NULL;
}
int main() {
pthread_t thread;
pthread_create(&thread, NULL, add_up, NULL);
printf("Minutes lost: %d\n", total);
pthread_join(thread, NULL);
return 0;
}
How many minutes does it print?
What happens:
Minutes lost: 0
Zero. main creates the thread and on the very next line prints total, without waiting. The thread has barely started (or not even that), so total is still 0. The pthread_join is there, but after the printf: it’s no use anymore. It’s the same bug as reversing the string and printing it before the join.
Using the thread’s result before pthread_join. There’s no error or warning: just a wrong number, and sometimes the right one if the thread is very fast.
Fix: swap two lines.
pthread_t thread;
pthread_create(&thread, NULL, add_up, NULL);
pthread_join(thread, NULL); // first wait...
printf("Minutes lost: %d\n", total); // ...and only then read
Output of the fix:
Minutes lost: 54
4 + 0 + 11 + 7 + 2 + 15 + 6 + 9 = 54 minutes.
Solutions
Level 1 solution
#include <stdio.h>
#include <string.h> // strlen
#include <pthread.h>
char stop[] = "San Telmo"; // global: the thread changes it and main reads it afterwards
void* reverse(void* arg) {
int left = 0;
int right = strlen(stop) - 1; // -1: the last useful character comes before the '\0'
while (left < right) { // when they cross, everything has been swapped
char aux = stop[left];
stop[left] = stop[right];
stop[right] = aux;
left++;
right--;
}
return NULL;
}
int main() {
pthread_t thread;
printf("Before: %s\n", stop);
pthread_create(&thread, NULL, reverse, NULL);
pthread_join(thread, NULL); // without this we could print the string half reversed
printf("After: %s\n", stop);
return 0;
}
Output:
Before: San Telmo After: omleT naS
“San Telmo” has 9 characters (with the space), so right starts at 8. The trace of the thread’s loop:
| Iteration | left | right | Swaps | stop afterwards |
|---|---|---|---|---|
| 1 | 0 | 8 | S ↔ o | oan TelmS |
| 2 | 1 | 7 | a ↔ m | omn TelaS |
| 3 | 2 | 6 | n ↔ l | oml TenaS |
| 4 | 3 | 5 | ↔ e | omleT naS |
| — | 4 | 4 | (they meet: left < right is false) | omleT naS |
With 9 characters you need 4 swaps; the middle one (the T, position 4) doesn’t move.
Why it’s done this way:
stopis global so the thread can change it without an argument (as the exam asks: “in the same memory”). Passing it withpthread_create(&thread, NULL, reverse, stop)and getting it back withchar* s = (char*)arg;would also work.right = strlen(stop) - 1because the last useful character is right before the'\0'. Withstrlen(stop)without the- 1you’d swap theSwith the'\0'and the string would be empty when printed.while (left < right)and not<=: when they’re equal it’s the middle character, and swapping it with itself does nothing.- The “After”
printfgoes afterpthread_join; otherwise the string would come out half reversed or not reversed at all (snippet 3).
Level 2 solution
#include <stdio.h>
#include <pthread.h>
#define N 8
// A query: the array, its size, the threshold and a slot for the answer
struct Query {
int* v;
int n;
int threshold;
int result;
};
void* count_less(void* arg) {
struct Query* c = (struct Query*)arg;
c->result = 0;
for (int i = 0; i < c->n; i++) {
if (c->v[i] < c->threshold) { // strict <: 5 is not less than 5
c->result++;
}
}
return NULL;
}
void* count_greater(void* arg) {
struct Query* c = (struct Query*)arg;
c->result = 0;
for (int i = 0; i < c->n; i++) {
if (c->v[i] > c->threshold) { // strict >: 5 isn't greater than 5 either
c->result++;
}
}
return NULL;
}
int main() {
int week2[N] = {3, 8, 0, 12, 5, 6, 1, 10};
// One struct per thread: if they shared one, they'd overwrite each other's result
struct Query less = {week2, N, 5, 0};
struct Query greater = {week2, N, 5, 0};
pthread_t h1, h2;
pthread_create(&h1, NULL, count_less, &less);
pthread_create(&h2, NULL, count_greater, &greater);
pthread_join(h1, NULL);
pthread_join(h2, NULL);
printf("Trips with less than %d minutes: %d\n", less.threshold, less.result);
printf("Trips with more than %d minutes: %d\n", greater.threshold, greater.result);
printf("Trips with exactly %d minutes: %d\n", less.threshold, N - less.result - greater.result);
return 0;
}
Output:
Trips with less than 5 minutes: 3 Trips with more than 5 minutes: 4 Trips with exactly 5 minutes: 1
Check with {3, 8, 0, 12, 5, 6, 1, 10} and threshold 5: less than 5 are 3, 0 and 1 (3 trips); more than 5, 8, 12, 6 and 10 (4 trips); and the 5 is neither: 8 − 3 − 4 = 1 trip with exactly 5 minutes.
Why it’s done this way:
- Two structs (
lessandgreater), even though they hold the same data: each thread writesc->result, and if they shared a struct the second would overwrite the first’s result. - Both structs point to the same array
week2: the threads only read it, so no copy and no mutex are needed. - The comparisons are strict (
<and>) so the 5 counts in neither. It’s exactly the detail the exam statement checks with “less than X”: if you write<=, the result changes. - The third value comes from a subtraction in
main, after thejoincalls: you don’t need another thread for something you can deduce.
Level 3 solution
#include <stdio.h>
#include <pthread.h>
#define DAYS 30
#define THREADS 4
int month[DAYS] = {4, 0, 11, 7, 2, 15, 6, 9, 3, 5, 1, 8, 12, 0, 2,
6, 10, 4, 7, 3, 0, 14, 5, 9, 1, 6, 2, 8, 11, 3};
int total = 0; // shared: sum of all the chunks
int worst = -1; // shared: the biggest delay of the month
pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER;
struct Chunk {
int id;
int start;
int end; // not included
};
void* summarise(void* arg) {
struct Chunk* t = (struct Chunk*)arg;
int sum = 0; // first work locally, without the lock...
int max = month[t->start];
for (int i = t->start; i < t->end; i++) {
sum += month[i];
if (month[i] > max) {
max = month[i];
}
}
pthread_mutex_lock(&lock); // ...and the critical section is entered ONCE
total += sum;
if (max > worst) {
worst = max;
}
pthread_mutex_unlock(&lock);
printf("Thread %d: days %d-%d, %d minutes, max %d\n", t->id, t->start + 1, t->end, sum, max);
return NULL;
}
int main() {
struct Chunk chunks[THREADS];
pthread_t h[THREADS];
int base = DAYS / THREADS; // 30 / 4 = 7 days for each one...
int remainder = DAYS % THREADS; // ...with 2 left over, given to the first ones
int start = 0;
for (int k = 0; k < THREADS; k++) {
int how_many = base + (k < remainder ? 1 : 0); // the first two take 8, the others 7
chunks[k].id = k + 1;
chunks[k].start = start;
chunks[k].end = start + how_many;
start += how_many;
pthread_create(&h[k], NULL, summarise, &chunks[k]);
}
for (int k = 0; k < THREADS; k++) {
pthread_join(h[k], NULL);
}
printf("Whole month: %d minutes of delay, mean %.2f per day, worst day %d minutes\n",
total, (double)total / DAYS, worst);
return 0;
}
Output of the single run:
Thread 1: days 1-8, 54 minutes, max 15 Thread 2: days 9-16, 37 minutes, max 12 Thread 4: days 24-30, 40 minutes, max 11 Thread 3: days 17-23, 43 minutes, max 14 Whole month: 174 minutes of delay, mean 5.80 per day, worst day 15 minutes
Notice that thread 4 printed before thread 3: each thread prints when it finishes, and the order changes from one run to another. The last line always comes out last, because main prints it after the four join calls.
The split done by main‘s loop (base = 30 / 4 = 7, remainder = 30 % 4 = 2):
k | k < remainder | how_many | start | end | Days |
|---|---|---|---|---|---|
| 0 | Yes | 8 | 0 | 8 | 1-8 |
| 1 | Yes | 8 | 8 | 16 | 9-16 |
| 2 | No | 7 | 16 | 23 | 17-23 |
| 3 | No | 7 | 23 | 30 | 24-30 |
And the sums of each chunk: 4 + 0 + 11 + 7 + 2 + 15 + 6 + 9 = 54 (max 15); 3 + 5 + 1 + 8 + 12 + 0 + 2 + 6 = 37 (max 12); 10 + 4 + 7 + 3 + 0 + 14 + 5 = 43 (max 14); 9 + 1 + 6 + 2 + 8 + 11 + 3 = 40 (max 11). Total: 54 + 37 + 43 + 40 = 174, and 174 / 30 = 5.80 per day. The worst day of the month, 15.
The thread function, up close:
void* summarise(void* arg) {
struct Chunk* t = (struct Chunk*)arg;
int sum = 0; // first work locally, without the lock...
int max = month[t->start];
for (int i = t->start; i < t->end; i++) {
sum += month[i];
if (month[i] > max) {
max = month[i];
}
}
pthread_mutex_lock(&lock); // ...and the critical section is entered ONCE
total += sum;
if (max > worst) {
worst = max;
}
pthread_mutex_unlock(&lock);
printf("Thread %d: days %d-%d, %d minutes, max %d\n", t->id, t->start + 1, t->end, sum, max);
return NULL;
}
Why it’s done this way:
- Local first, shared afterwards.
sumandmaxare the thread’s local variables (they live on its stack): while it walks its chunk it bothers nobody. Only at the end does it close the lock, add its share and compare its maximum. That way each thread enters the critical section once instead of 7 or 8 times. - If each thread did
total += month[i]inside the loop withlock/unlockon every iteration, the result would be the same but the threads would spend most of their time waiting for each other. worststarts at −1 so the first maximum that arrives always replaces it (every delay is 0 or more).max = month[t->start]and notmax = 0: starting with a value from the chunk itself is the safe way to look for a maximum, even though there are no negatives here.(k < remainder ? 1 : 0)hands out the leftovers: with 28 days instead of 30,remainderwould be 0 and everyone would take 7. The split works for any number of days.- The
printfinside the thread comes after theunlock: printing doesn’t touch shared data, so there’s no need to keep the lock closed meanwhile.
Without the mutex, two threads could do total += sum at the same time and lose a whole chunk. With four additions it’s very unlikely to show, but not impossible, and the exam values having it there.
Watch it step by step in Python Tutor
Python Tutor runs code line by line and draws the variables and memory (choose C). It doesn’t run threads, so we simulate snippet 2 in the worst possible case: the four “threads” store what they would receive (&i) and don’t read until the loop has finished.
#include <stdio.h>
// Python Tutor doesn't run threads: we pretend all four start once the loop has finished
int main() {
int* received[4]; // what each "thread" keeps: an address
int i;
for (i = 0; i < 4; i++) {
received[i] = &i; // like pthread_create(..., &i): it stores where i is, not its value
}
for (int k = 0; k < 4; k++) {
printf("Thread %d reads %d\n", k, *received[k]); // they all read the same slot
}
return 0;
}
Output, run with gcc:
Thread 0 reads 4 Thread 1 reads 4 Thread 2 reads 4 Thread 3 reads 4
What to watch step by step:
- On each iteration of the first
for, look at thereceivedarray: its four slots are arrows pointing to the same box, the box ofi. - When the loop ends,
iis 4. No slot stored a 0, a 1, a 2 or a 3. - In the second
for, each*received[k]follows the arrow and reads what is ininow: 4 all four times. - Replace
received[i] = &i;with anidsarray,ids[i] = i;andreceived[i] = &ids[i];, and run it again: you’ll see four arrows to four different boxes.
Cheat sheet: threads in C with pthread
The essentials in 30 seconds
#include <pthread.h>, the functionvoid* f(void* arg) { …; return NULL; },pthread_t h;,pthread_create(&h, NULL, f, arg);,pthread_join(h, NULL);and compile with-lpthreadat the end.- What goes into the thread is an address:
&xfor a number,vfor an array,&sfor a struct (one per thread). - Results are read after
pthread_join. - If two threads write the same variable: a global mutex,
lockbefore andunlockafter.
I want… / Code / Result
| I want… | Code | Result |
|---|---|---|
| Create a thread | pthread_create(&h, NULL, f, NULL); | f starts running alongside main |
| Wait for it to finish | pthread_join(h, NULL); | main stops until f returns |
Pass an int | pthread_create(&h, NULL, f, &x); | Inside: int x = *(int*)arg; |
| Pass a character | pthread_create(&h, NULL, f, &letter); | Inside: char c = *(char*)arg; |
| Pass a string | pthread_create(&h, NULL, f, text); | Inside: char* s = (char*)arg; |
| Pass an array | pthread_create(&h, NULL, f, v); | Inside: int* v = (int*)arg; |
| Pass several values | pthread_create(&h, NULL, f, &data); | Inside: struct D* d = (struct D*)arg; and d->field |
| Return a result | d->result = n; in the thread | main reads it after the join |
| Create N threads | for (k…) pthread_create(&h[k], NULL, f, &data[k]); | One struct per thread |
| Wait for N threads | for (k…) pthread_join(h[k], NULL); | In another loop, not the creating one |
| Protect a variable | pthread_mutex_lock(&m); x++; pthread_mutex_unlock(&m); | No lost increments |
| Declare the lock | pthread_mutex_t m = PTHREAD_MUTEX_INITIALIZER; | Global, open |
| Compile | gcc -Wall -o prog prog.c -lpthread | Executable prog |
Templates from the exercises
One thread with one argument (level 1 and program 1 of the practice):
void* task(void* arg) {
int x = *(int*)arg;
// … walk the global array, count, print …
return NULL;
}
int main() {
int x;
scanf("%d", &x); // the data, BEFORE creating the thread
pthread_t h;
pthread_create(&h, NULL, task, &x);
pthread_join(h, NULL);
return 0;
}
Several threads with a struct and a mutex (level 3):
struct Chunk { int start; int end; };
int total = 0;
pthread_mutex_t m = PTHREAD_MUTEX_INITIALIZER;
void* task(void* arg) {
struct Chunk* t = (struct Chunk*)arg;
int sum = 0; // locally…
for (int i = t->start; i < t->end; i++) sum += v[i];
pthread_mutex_lock(&m); // …and only once on shared data
total += sum;
pthread_mutex_unlock(&m);
return NULL;
}
Results of the exercises: level 1, omleT naS; level 2, 3 less, 4 greater and 1 exactly 5; level 3, chunks of 8, 8, 7 and 7 days with 54, 37, 43 and 40 minutes, total 174, mean 5.80 and worst day 15.
Symptom / Likely cause
| Symptom | Likely cause |
|---|---|
| The thread prints nothing | pthread_join is missing: main ended first |
| You get 0 or the string not reversed | The result is used before the join |
incompatible pointer type in argument 3 | The signature isn’t void* f(void* arg) |
makes pointer from integer in argument 4 | The argument’s & is missing (x instead of &x) |
undefined reference to pthread_create | -lpthread is missing (on systems with glibc older than 2.34) |
| Several threads claim the same number | They all received &i from the loop |
| The total is different on every run | Race condition: the mutex is missing |
| The program hangs | A lock without its unlock |
| The lines come out in a different order from the statement | Normal with threads; if it matters, print in main after the join calls |
Before you hand it in, ask yourself
- Is the thread function
void* name(void* arg)and does it end withreturn NULL;? - Is there one
pthread_joinperpthread_create, and does it come before the results are used? - Does each thread receive its own address (not
&i), and do the data have a value before it’s created? - Have you compiled with
-Wall -lpthreadwith no warnings and taken screenshots of the code and the run?
And if you want to try it against the clock, the IC2 practical mock exam has three versions with its threads exercise.
Keep goingAll C articlesDid this article help you? Here is how you can support SergioLearns →
Ready for the exam?IC2 practical mock exam — flowcharts, fork and threads in C (and the screenshots you can’t leave out)


