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*nameare required. A function's name, uncalled, decays to its address, sofp = some_function;(no&, no()) is how you pointfpat 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 callsht->hash_fn(...)will run. - A thread function always has the signature
void *fn(void *arg)- itself a function pointer, passed topthread_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
&ifrom a loop variable that keeps changing. Give each thread its own copy - an array indexed byi, or onemallocper thread. - Create every thread first, then join every thread in a second loop - joining inside the creation loop throws away the concurrency.
Set up¶
Start Part A and B2-B4 from these includes:
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;
}
- What does each of the three
printfcalls print? Trace which functionf(and theng) points at, line by line. op_t g = f;copiesf's value intog. What, concretely, is being copied - the code of the function, or something else? What would happen tog's behavior if, after this line, you reassignedf = add;again?
Exercise A2 - Which interleavings are possible?¶
Two threads run concurrently. Thread 1 runs:
Thread 2 runs:
main creates both threads and joins both before printing "done".
- 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 (AbeforeB,1before2). - List one ordering that is not legal, and say which rule it breaks.
- Does
pthread_createorpthread_joinguarantee any particular one of your legal orderings actually happens? What is the only ordering guaranteejoingives 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;
}
- What does every thread actually read for
id, and why? (Hint: how many copies ofiexist while the loop is running?) - Rewrite
mainso each thread reliably prints its own, distinctid.
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);
- Add
hash_fn_t hash_fn;tostruct hash_table. Giveht_createa second parameter,hash_fn_t hash_fn, and store it:ht->hash_fn = hash_fn;. - In both
ht_getandht_put, replacehash_string(key)withht->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 separateht_create(8, sum_hash),ht_put/ht_geta 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.
- Cast
argback toint *, compute the sum in along,mallocalong, store the sum in it, andreturnthe pointer. - In
main:malloc(or stack-allocate, sincemainoutlives the join) thento pass in,pthread_create,pthread_join(t, (void **) &result), print*result, thenfree(result). - Check your understanding: why must the thread's result be handed back
through a
malloc'd pointer rather than, say, a locallonginsidesum_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.
(Your lines may print in any order - that is expected and fine.)
- Allocate
int ids[5]inmain(ormallocan array), setids[i] = iin 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.
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) withdata,start,end; the thread writes its own sum intopartialinside its own struct - no two threads touch the samechunk_t. sum_chunkloopsfor (int i = c->start; i < c->end; i++) c->partial += c->data[i];and returnsNULL(the result already lives in the struct, so we do not need amalloc'd return value here).- After joining all four threads,
mainadds up the four.partialfields for the grand total. Cross-check it against a plain sequential loop over the whole array. - Check your understanding: why is it safe for
mainto read everychunk_t.partialright 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.