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.

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 signature void* f(void* arg) with return NULL;.
  • How to get the argument back: *(int*)arg for an int, (int*)arg for an array and (struct X*)arg for 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

  1. main reads the threshold with scanf before creating the thread (if you created it first, the thread could read the variable before it has a value).
  2. main passes &threshold to pthread_create and waits at pthread_join.
  3. 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.

What the thread counts with threshold 6The eight delays 4, 0, 11, 7, 2, 15, 6 and 9. In coral the ones above 6: 11, 7, 15 and 9. The worst is trip 6, with 15 minutes.delays[] and threshold = 6 (typed)4trip 10trip 211trip 37trip 42trip 515trip 66trip 79trip 8the worst: 15 min4 trips over 6 minutes: 11, 7, 15 and 9 (the 6 doesn't count: it isn't greater than 6)
In coral, what the thread prints; the thick border marks the worst trip.

The thread’s walk, iteration by iteration (the printf in the “Prints” column keeps adding numbers to the same line):

idelays[i]> 6?nworst (position)Prints
04No00—
10No00—
211Yes12 11
37Yes22 7
42No22—
515Yes35 15
66No35—
79Yes45 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 on main‘s variable.
  • worst starts at 0 (the first trip) and only changes with >: if there were two trips with 15, the first would stay.
  • The two if statements go one after the other, separately, not with else: 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.

Two different tasks on the same arrayBoth threads read the same array in main. The statistics one leaves the mean 6.75 and the maximum 15 at position 5 in its struct; the categories one leaves 2 on time, 1 minor and 5 serious in its own. main waits for both and then prints.delays[8] (in main)4 0 11 7 2 15 6 9statistics(&s)mean = 6.75max = 15, pos_max = 5categories(&c)on_time = 2, minor = 1serious = 5main: join, join and printf
Both read the same array and each one writes only to ITS struct: that's why no mutex is needed.

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:

iv[i]Branchon_timeminorserious
04<= 5010
10<= 2110
211else111
37else112
42<= 2212
515else213
66else214
79else215

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

  1. One Line struct per thread with its name and its 7 delays (each thread receives &lines[k]).
  2. The histogram (int histogram[3]), the total and the lock are global: all four threads must use the same ones.
  3. Each thread, for each day, works out the category outside the lock (it touches nothing shared) and only closes the lock to add.
  4. main creates 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:

LineDelaysOn time (0-2)Minor (3-5)Serious (6+)Minutes
13 5 2 8 4 6 123229
129 12 7 10 14 8 1100771
170 2 1 3 0 4 252012
916 4 9 5 7 3 803442
Total28 trips7813154

7 + 8 + 13 = 28 trips, and 29 + 71 + 12 + 42 = 154 minutes. Line 12 didn’t make a single on-time trip all week.

Shared histogram of the four linesStacked bars with what each thread adds to the histogram: on time 2 + 0 + 5 + 0 = 7, minor 3 + 0 + 2 + 3 = 8 and serious 2 + 7 + 0 + 4 = 13.on time (0-2)25= 7minor (3-5)323= 8serious (6+)274= 13Line 1Line 12Line 17Line 91
Each colour is a thread: all four add to the same three slots, which is why they need the lock.

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 the if statements go before the lock: working out the category only looks at l->v[d], which belongs to that thread’s struct. Nobody else touches it, so there’s nothing to protect.
  • Between lock and unlock only 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 lock and the unlock, 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 minutes field to the Line struct for each thread to fill in, and have main print which line lost the most time.

Common mistakes in this practice

  • Passing the value instead of the address (threshold instead of &threshold): it doesn’t compile with a modern gcc.
  • Creating the thread before having the data (scanf after pthread_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.
  • create and join in 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:

  1. When e is created, notice that its field v is an arrow to main‘s delays array: the struct doesn’t copy the data, it points to them.
  2. When entering statistics, arg and the inner e point to the same struct in main. That’s the (struct Statistics*)arg conversion.
  3. Watch e->pos_max change at i = 2 (the 11) and at i = 5 (the 15), and never again.
  4. Back in main, e.max and e.pos_max already hold 15 and 5: the result stayed in the struct, with no return.

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 main prints after the join calls, 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 for create and another for join.

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 -Wall and -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 →