CMSC 14300: Practice Set 7 - Threads, Race Conditions, and Mutexes¶
Practice for the material in Week 6: creating and joining threads from Lecture 11, and the race conditions and mutexes from Lecture 12. None of these repeats a lecture or homework exercise; together they drill the new moves - packaging thread arguments, splitting work across threads with no shared writes, recognizing a critical section, protecting it with a mutex, and locking more than one mutex without deadlocking. This is good preparation for later material that builds on concurrency.
Work in a fresh directory and compile everything with warnings on, linking the thread library:
mkdir -p ~/cmsc14300/pset07 && cd ~/cmsc14300/pset07
clang -Wall -Wextra -std=c17 -pthread problem1.c -o problem1
Keep this week's rules in view: a thread function is always void *fn(void *);
create all threads, then join all threads; never hand a thread the address of
a loop variable that keeps changing; and any memory more than one thread writes
needs a mutex around every access to it, read or write.
#include <pthread.h>
pthread_t t;
pthread_create(&t, NULL, fn, arg);
pthread_join(t, NULL);
pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER;
pthread_mutex_lock(&lock);
/* critical section */
pthread_mutex_unlock(&lock);
Problems 1 to 2 use thread creation and joining only, with no shared writes. Problems 3 to 5 add a mutex.
Problem 1 - Factorial, one thread per input¶
Write a thread function that computes n! for an n passed in and hands the
result back through its return value. main reads several numbers, launches one
thread per number, joins them all, and prints every result.
- Cast
argback toint *, compute the product in along,mallocalongfor the result, store it, andreturnthe pointer. The caller frees each result after printing it. - Give each thread its own
intto read (an array of the inputs, one slot per thread) - never the address of a shared loop variable. - Check your understanding: why would returning the address of a local
longcomputed insidefactorialbe unsafe, the same way it was unsafe to return the address of a local from an ordinary function?
Problem 2 - Warmest reading per sensor group (no locks needed)¶
You have readings from 40 temperature sensors, split into 4 groups of 10. Launch
one thread per group to find that group's maximum reading; main joins all
four and finds the overall maximum by comparing the four group maximums. No two
threads should ever read or write the same memory.
typedef struct { int *readings; int start, end; int group_max; } group_t;
void *find_group_max(void *arg);
group 0 [0,10): max = 91
group 1 [10,20): max = 88
group 2 [20,30): max = 97
group 3 [30,40): max = 85
overall max: 97
- Each thread's
group_thas its ownreadingspointer (into the shared array, but read-only), its ownstart/endslice, and its owngroup_maxfield that only that thread ever writes. - After joining all four,
mainscans the fourgroup_maxfields (a plain loop over 4 elements) for the overall maximum. - Check your understanding: this program touches a shared array (
readings) from multiple threads with no mutex, and that is fine. What property of how the threads usereadingsmakes that safe?
Problem 3 - A shared download counter (race, then fix)¶
Simulate 6 threads each "completing" 100,000 downloads by incrementing a shared
long downloads_done. First write it with no protection and observe the count
fall short of 600000 on some runs. Then add a mutex and confirm it is exact
every time.
(no lock) downloads_done = 583201 (varies by run, usually short)
(w/ lock) downloads_done = 600000 (every run)
- Unprotected version:
downloads_done++;in the loop. Protected version: wrap just that line inpthread_mutex_lock/unlock. - Run the unprotected binary two or three times (rerunning the whole program) to see the final count actually change between runs.
- Check your understanding:
downloads_done++compiles down to a load, an add, and a store. Describe, in your own words, an interleaving of two threads' loads and stores that loses one increment.
Problem 4 - Move books between two shared library counts¶
Two shared counters, main_branch and annex_branch, each start at 500 (books
on the shelf at each location). Multiple threads call move_to_annex(int n) or
move_to_main(int n) concurrently, each of which must lock both counters'
mutexes before changing either count. Do this without deadlocking, no matter how
many threads call each function at the same time.
int main_branch = 500, annex_branch = 500;
pthread_mutex_t lock_main = PTHREAD_MUTEX_INITIALIZER;
pthread_mutex_t lock_annex = PTHREAD_MUTEX_INITIALIZER;
void move_to_annex(int n); /* main_branch -= n; annex_branch += n; */
void move_to_main(int n); /* annex_branch -= n; main_branch += n; */
8 threads, half moving each direction, 1000 moves of 1 book each
main_branch + annex_branch == 1000 at the end (every run, program terminates)
- Both functions must lock the same mutex first, regardless of which
direction books are moving - for example, always
lock_mainbeforelock_annex. Only the arithmetic differs between the two functions; the locking order does not. - Launch threads calling both functions concurrently and confirm the program
finishes (does not hang) and the two counts always sum to
1000. - Check your understanding: if
move_to_annexlockedlock_mainthenlock_annex, butmove_to_mainlockedlock_annexthenlock_main, describe a specific pair of calls, running at the same time, that would deadlock.
Problem 5 - A shared "next free slot" counter¶
N worker threads each want to claim a unique slot index in a shared results
array of size N, by reading and incrementing a shared int next_slot (starting
at 0) and using the value they read as their own index - no two threads may
ever claim the same slot, even though all N threads race to claim one at
(nearly) the same time. Each thread then writes its thread number into
results[slot].
int next_slot = 0;
pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER;
int results[N];
void *claim_slot(void *arg); /* writes its own id into results[the slot it claimed] */
- The read of
next_slotand the increment must happen together as one critical section: lock, readnext_slotinto a localmy_slot, incrementnext_slot, unlock - then usemy_slot(already a private local) to writeresults[my_slot]outside the lock. - Check your understanding: why is it not enough to just lock around
next_slot++alone if a thread also needs to know which value it claimed? What would go wrong if two threads each rannext_slot++under the lock but then both used the sharednext_slot(read again, unprotected) as "their" slot?
Self-check¶
You're ready for later material if you can, without looking it up:
- Write the signature of a pthread thread function from memory, and say why it must be exactly that signature.
- Explain why threads created in a loop must never be handed the address of the loop variable, and show the fix.
- Say, in one sentence, what a race condition is and why
x++on sharedxis not one atomic step. - Wrap a critical section in a mutex lock/unlock, and explain why the locked region should be as small as possible.
- Explain what causes a deadlock between two mutexes and how a consistent lock order prevents it.