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.

How to use this article

  1. 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.
  2. Compile it with gcc -Wall -o program program.c -lpthread and compare your output with the expected one. If you get stuck, open the hint.
  3. Before reading the solutions, try to predict what each snippet in “What’s wrong here?” does.
  4. 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 in a loop versus passing &ids[i]On the left, the four threads point to the same variable i, which the loop keeps changing up to 4. On the right, each thread points to its own slot of the ids array, holding 0, 1, 2 and 3.Wrong: &iRight: &ids[i]thread 0thread 0ids[0] = 0thread 1thread 1ids[1] = 1thread 2thread 2ids[2] = 2thread 3thread 3ids[3] = 3i0 → 1 → … → 4They read i whenever they run: 2, 2, 2, 4…Each one, its own slot: 0, 1, 2, 3
A shared address gives you the value at that moment, not the one when the thread was created.

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:

IterationleftrightSwapsstop afterwards
108S ↔ ooan TelmS
217a ↔ momn TelaS
326n ↔ loml TenaS
435 ↔ eomleT naS
—44(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:

  • stop is global so the thread can change it without an argument (as the exam asks: “in the same memory”). Passing it with pthread_create(&thread, NULL, reverse, stop) and getting it back with char* s = (char*)arg; would also work.
  • right = strlen(stop) - 1 because the last useful character is right before the '\0'. With strlen(stop) without the - 1 you’d swap the S with 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” printf goes after pthread_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 (less and greater), even though they hold the same data: each thread writes c->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 the join calls: 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 month split across four threadsThirty bars with the month's delays, coloured by thread: the first two take 8 days and the other two 7. Each thread adds up its chunk (54, 37, 43 and 40) and enters the lock once to leave the total 174 and the worst day 15.30 days across 4 threads: 30 / 4 = 7 with 2 left overthread 1: days 1-854 min, max 15thread 2: days 9-1637 min, max 12thread 3: days 17-2343 min, max 14thread 4: days 24-3040 min, max 11lock · total += sum · worst = max · unlocktotal = 174, worst = 15
Each thread works locally and only touches the shared data once, at the end.

The split done by main‘s loop (base = 30 / 4 = 7, remainder = 30 % 4 = 2):

kk < remainderhow_manystartendDays
0Yes8081-8
1Yes88169-16
2No7162317-23
3No7233024-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. sum and max are 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 with lock/unlock on every iteration, the result would be the same but the threads would spend most of their time waiting for each other.
  • worst starts at −1 so the first maximum that arrives always replaces it (every delay is 0 or more).
  • max = month[t->start] and not max = 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, remainder would be 0 and everyone would take 7. The split works for any number of days.
  • The printf inside the thread comes after the unlock: 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:

  1. On each iteration of the first for, look at the received array: its four slots are arrows pointing to the same box, the box of i.
  2. When the loop ends, i is 4. No slot stored a 0, a 1, a 2 or a 3.
  3. In the second for, each *received[k] follows the arrow and reads what is in i now: 4 all four times.
  4. Replace received[i] = &i; with an ids array, ids[i] = i; and received[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 function void* f(void* arg) { …; return NULL; }, pthread_t h;, pthread_create(&h, NULL, f, arg);, pthread_join(h, NULL); and compile with -lpthread at the end.
  • What goes into the thread is an address: &x for a number, v for an array, &s for a struct (one per thread).
  • Results are read after pthread_join.
  • If two threads write the same variable: a global mutex, lock before and unlock after.

I want… / Code / Result

I want…CodeResult
Create a threadpthread_create(&h, NULL, f, NULL);f starts running alongside main
Wait for it to finishpthread_join(h, NULL);main stops until f returns
Pass an intpthread_create(&h, NULL, f, &x);Inside: int x = *(int*)arg;
Pass a characterpthread_create(&h, NULL, f, &letter);Inside: char c = *(char*)arg;
Pass a stringpthread_create(&h, NULL, f, text);Inside: char* s = (char*)arg;
Pass an arraypthread_create(&h, NULL, f, v);Inside: int* v = (int*)arg;
Pass several valuespthread_create(&h, NULL, f, &data);Inside: struct D* d = (struct D*)arg; and d->field
Return a resultd->result = n; in the threadmain reads it after the join
Create N threadsfor (k…) pthread_create(&h[k], NULL, f, &data[k]);One struct per thread
Wait for N threadsfor (k…) pthread_join(h[k], NULL);In another loop, not the creating one
Protect a variablepthread_mutex_lock(&m); x++; pthread_mutex_unlock(&m);No lost increments
Declare the lockpthread_mutex_t m = PTHREAD_MUTEX_INITIALIZER;Global, open
Compilegcc -Wall -o prog prog.c -lpthreadExecutable 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

SymptomLikely cause
The thread prints nothingpthread_join is missing: main ended first
You get 0 or the string not reversedThe result is used before the join
incompatible pointer type in argument 3The signature isn’t void* f(void* arg)
makes pointer from integer in argument 4The 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 numberThey all received &i from the loop
The total is different on every runRace condition: the mutex is missing
The program hangsA lock without its unlock
The lines come out in a different order from the statementNormal with threads; if it matters, print in main after the join calls

Before you hand it in, ask yourself

  1. Is the thread function void* name(void* arg) and does it end with return NULL;?
  2. Is there one pthread_join per pthread_create, and does it come before the results are used?
  3. Does each thread receive its own address (not &i), and do the data have a value before it’s created?
  4. Have you compiled with -Wall -lpthread with 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 articles

Did 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)