Skip to content

Handout: Lecture 11 In-Class Exercises

Part A is pen-and-paper: trace calls through a function pointer, then reason about the possible orderings of two threads' output before you ever run code. Part B is at the keyboard: make Lecture 10's hash table use a swappable hash function, create a thread and join it, pass a thread its own argument correctly, and launch several threads that each own a disjoint slice of an array. None of these repeats an earlier exercise; they drill today's moves - declaring and calling through a function pointer, pthread_create, pthread_join, packaging arguments and results behind void *, and the create-all-then-join-all pattern.

Try each one yourself first; we will discuss in class, and the solutions are in a separate document afterward. Compile with warnings on; B1 needs no thread library, but B2 onward links it:

clang -Wall -Wextra -std=c17 myprog.c -o myprog          # B1 only
clang -Wall -Wextra -std=c17 -pthread myprog.c -o myprog # B2 onward

Reminders that will keep you out of trouble today:

  • A function pointer variable is declared ret_type (*name)(param_types) - the parentheses around *name are required. A function's name, uncalled, decays to its address, so fp = some_function; (no &, no ()) is how you point fp at it.
  • A struct can hold a function pointer as a field (for example hash_fn_t hash_fn), letting code that fills in the struct choose the behavior that code which only ever calls ht->hash_fn(...) will run.
  • A thread function always has the signature void *fn(void *arg) - itself a function pointer, passed to pthread_create.
  • pthread_create(&t, NULL, fn, arg) starts the thread; pthread_join(t, &retval) waits for it and collects what it returned.
  • Threads share the heap and globals, but each has its own stack. Today's examples only work each thread on its own slice of memory - no two threads read or write the same location.
  • Never hand a thread &i from a loop variable that keeps changing. Give each thread its own copy - an array indexed by i, or one malloc per thread.
  • Create every thread first, then join every thread in a second loop - joining inside the creation loop throws away the concurrency.

Set up

mkdir -p ~/cmsc14300/lec11
cd ~/cmsc14300/lec11

Start Part A and B2-B4 from these includes:

#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>

Start B1 from these includes, type definitions, and given ht_create, entry_t, and hash_string (Lecture 10's djb2), which you will modify to add a function pointer field:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

struct entry {
    char         *key;
    int           value;
    struct entry *next;
};
typedef struct entry entry_t;

struct hash_table {
    entry_t **buckets;
    int       nbuckets;
    /* you will add a hash_fn field here */
};
typedef struct hash_table hash_table_t;

/* given: djb2 from Lecture 10 */
unsigned long hash_string(const char *key) {
    unsigned long h = 5381;
    for (int i = 0; key[i] != '\0'; i++) {
        h = h * 33 + (unsigned char) key[i];
    }
    return h;
}

/* given: insert-or-update and lookup, unchanged from Lecture 10 except for
   whatever line you change to call through the new function pointer */
void ht_put(hash_table_t *ht, const char *key, int value) { /* ... */ }
int  ht_get(hash_table_t *ht, const char *key, int *out_value) { /* ... */ }

Part A - Reasoning with function pointers and threads (pen and paper)

Exercise A1 - Trace the function pointer

int add(int a, int b) { return a + b; }
int sub(int a, int b) { return a - b; }

typedef int (*op_t)(int, int);

int main(void) {
    op_t f = add;
    printf("%d\n", f(10, 3));
    f = sub;
    printf("%d\n", f(10, 3));
    op_t g = f;
    printf("%d\n", g(1, 1));
    return 0;
}
  1. What does each of the three printf calls print? Trace which function f (and then g) points at, line by line.
  2. op_t g = f; copies f's value into g. What, concretely, is being copied - the code of the function, or something else? What would happen to g's behavior if, after this line, you reassigned f = add; again?

Exercise A2 - Which interleavings are possible?

Two threads run concurrently. Thread 1 runs:

printf("A\n");
printf("B\n");

Thread 2 runs:

printf("1\n");
printf("2\n");

main creates both threads and joins both before printing "done".

  1. List every ordering of the four lines (A, B, 1, 2) that is a legal interleaving - one where each thread's own two lines still appear in its own order (A before B, 1 before 2).
  2. List one ordering that is not legal, and say which rule it breaks.
  3. Does pthread_create or pthread_join guarantee any particular one of your legal orderings actually happens? What is the only ordering guarantee join gives you?

Exercise A3 - Spot the bug

void *print_id(void *arg) {
    int id = *(int *) arg;
    printf("thread %d\n", id);
    return NULL;
}

int main(void) {
    pthread_t threads[4];
    for (int i = 0; i < 4; i++) {
        pthread_create(&threads[i], NULL, print_id, &i);   /* <-- */
    }
    for (int i = 0; i < 4; i++) {
        pthread_join(threads[i], NULL);
    }
    return 0;
}
  1. What does every thread actually read for id, and why? (Hint: how many copies of i exist while the loop is running?)
  2. Rewrite main so each thread reliably prints its own, distinct id.

Part B - At the keyboard

Exercise B1 - A swappable hash function

Add a function pointer field to hash_table_t so the table's hash function is a setting, not a hardcoded call to hash_string. Update ht_create, ht_get, and ht_put to use it, then write a second hash function and build two tables that use two different hash functions with the exact same ht_get/ht_put code.

typedef unsigned long (*hash_fn_t)(const char *);
hash_table_t *ht_create(int nbuckets, hash_fn_t hash_fn);
ht1 (hash_string) put "cat" 3, get "cat" -> 3
ht2 (sum_hash)    put "cat" 3, get "cat" -> 3
  • Add hash_fn_t hash_fn; to struct hash_table. Give ht_create a second parameter, hash_fn_t hash_fn, and store it: ht->hash_fn = hash_fn;.
  • In both ht_get and ht_put, replace hash_string(key) with ht->hash_fn(key) - the only line that changes in each function.
  • Write sum_hash, a second hash function with the same signature (sum the character codes, no multiply-by-33 step). It does not need to be a good hash - the point is that it is a different function with the same signature.
  • Build ht_create(8, hash_string) and a separate ht_create(8, sum_hash), ht_put/ht_get a few keys into each, and confirm both work correctly with no other code changed.
  • Check your understanding: which two functions in your program had to change to add this feature, and which functions (name them) did not have to change at all? Why not?

Exercise B2 - Create one thread, join it, use its result

Write a thread function that computes the sum 1 + 2 + ... + n for an n passed in, hands the sum back through its return value, and a main that creates the thread, joins it, and prints the result.

void *sum_to_n(void *arg);   /* arg: int *n.  returns: malloc'd long * */
n = 100 -> thread computed 5050
  • Cast arg back to int *, compute the sum in a long, malloc a long, store the sum in it, and return the pointer.
  • In main: malloc (or stack-allocate, since main outlives the join) the n to pass in, pthread_create, pthread_join(t, (void **) &result), print *result, then free(result).
  • Check your understanding: why must the thread's result be handed back through a malloc'd pointer rather than, say, a local long inside sum_to_n?

Exercise B3 - Give each thread its own argument

Launch 5 threads. Thread i should square i and print "thread i: i*i = ...". Fix the loop-variable bug from A3 by giving each thread its own slot to read.

void *square_it(void *arg);
thread 0: 0*0 = 0
thread 1: 1*1 = 1
thread 2: 2*2 = 4
thread 3: 3*3 = 9
thread 4: 4*4 = 16

(Your lines may print in any order - that is expected and fine.)

  • Allocate int ids[5] in main (or malloc an array), set ids[i] = i in the creation loop, and pass &ids[i] - a distinct address per thread, not a shared loop variable.
  • Create all 5 threads first, then join all 5 in a second loop.

Exercise B4 - Parallel array sum, one slice per thread

Split a 100-element int array into 4 equal slices. Launch one thread per slice to sum its own slice; main joins all four and adds their partial sums together for the total.

typedef struct { int *data; int start, end; long partial; } chunk_t;
void *sum_chunk(void *arg);
array of 100 elements, 4 threads
thread 0 slice [0, 25):   partial sum ...
thread 1 slice [25, 50):  partial sum ...
thread 2 slice [50, 75):  partial sum ...
thread 3 slice [75, 100): partial sum ...
total: ...
  • Fill an array of 4 chunk_t (one per thread) with data, start, end; the thread writes its own sum into partial inside its own struct - no two threads touch the same chunk_t.
  • sum_chunk loops for (int i = c->start; i < c->end; i++) c->partial += c->data[i]; and returns NULL (the result already lives in the struct, so we do not need a malloc'd return value here).
  • After joining all four threads, main adds up the four .partial fields for the grand total. Cross-check it against a plain sequential loop over the whole array.
  • Check your understanding: why is it safe for main to read every chunk_t.partial right after the join loop, with no lock of any kind?

Stretch - take it further

Time it

Write a version of B4 that also sums the array with a single sequential loop, and time both versions (#include <time.h>, clock() before/after, difference in seconds). Run with a much larger array (say a million elements) and more threads than cores, and see whether more threads keeps helping or starts to hurt.

A results array instead of a struct field

Rewrite B3 so that instead of each thread printing its own result, it stores the square into a shared int results[5] array at index i (still a disjoint write per thread - each thread only touches its own index), and main prints the whole array once every thread has been joined.

A compare function pointer

Add a second function pointer field to a struct of your choice from an earlier lecture (for example a point3d_t or item_t) named cmp, with signature int (*cmp)(const void *, const void *) - the same shape the standard library's qsort expects. Write two different comparison functions (say, by one field vs. another) and show that a single sorting or searching function written against cmp works with either one, unmodified - the same swappable-behavior idea as B1's hash function.