changelog:

  • 6 Apr 2026: fix bug in fail_test() call in pool-test.c
  • 8 Apr 2026: add note about ThreadSanitizer
  • 8 Apr 2026: fix test cases which were too picky about task numbering in pool-test.c
  • 8 Apr 2026: add pool-example.c
  • 9 Apr 2026: fix an additional test case which was too picky about task numbering in pool-test.c
  • 9 Apr 2026: reorganize section on testing; add some more advice on testing
  • 10 Apr 2026: make pool-test.c wait longer for tests that pause; cleanup grammar in with section on testing; note that pool-test.c is bad at testing waiting
  • 11 Apr 2026: add some test cases to pool-test.c to hopefully notice broken pool_wait()s more often
  • 14 Apr 2026: adjust two thread, four tasks, one submitting extra task, tasks 0/1, 4/5 using barriers in pool-test.c not to be too picky about ordering
  • 15 Apr 2026: remove redundant and all tasks are waited for from description of pool_stop
  • 17 Apr 2026: fix incorrect #ifdef to #ifndef in header guard
  • 26 Apr 2026: update pool-test.c to avoid calling pool_stop before all tasks are submitted in some cases

1 Your Task

  1. Implement a thread pool library with the following functions:

    • void pool_setup(int threads);

      Create exactly threads threads, each of which will later run tasks submitted by pool_submit_task.

      You may assume that this is called before any of the below functions are called.

    • int pool_submit_task(task_fn task, void *argument);

      Record a task as submitted so it will later be run by one of the threads created by the thread pool, then return.

      Tasks must be started in the order they are submitted.

      When the task is eventually run from one of the threads created by pool_setup, call the function specified by the function pointer task with the single argument argument. Then record the return value for later retrieval with pool_get_task_result.

      This function must return an identifier that represents this instance of the task. This identifier can be used to retrieve the return value (as long as pool_setup is not called again).

      This function must be safe to call from a task.

    • void pool_wait();

      Wait for all submitted tasks up to this point to complete running, if they haven’t already.

      You may assume this will not be called from a task.

      If new tasks are submitted while pool_wait() is running, you may either:

      • let those tasks run normally (possibly only finishing after pool_wait returns); or
      • delay those tasks until after pool_wait() returns
    • void *pool_get_task_result(int task_id);

      Retrieve the return value from the task with the given task_id.

      Return NULL if the task as not finished or does not exist.

      We do not care what this function does if there has been a new call to pool_setup since the task returned.

    • void pool_stop();

      Cause all threads in the thread pool to finish processing currently submitted tasks and then terminate. Wait for all the threads to be joined.

      Note that the task return values may still be retrieved after pool_stop returns.

      After pool_stop() returns, it should be possible to call pool_setup() again (possibly with a different number of threads) and then submit and wait functions normally again.

      We do not care how this function behaves if new tasks are submitted after it is started but before a subsequent call to pool_setup(). We also do not care how this function behaves if there is a pending call to pool_wait() in another thread.

    Your implementation does not need to support more than 200 tasks. You may assume no more than 200 tasks will be submitted.

    Whenever a thread needs to wait for an event (such as one of the threads started by pool_setup waiting for a new task), it may not consume a lot of compute time (busy wait). The most likely way you’d do this is by having the thread call a synchronization function that will waits until the event has likely happened.

  2. Test your implementation.

    To help you with this, we supply:

    • this pool-example.c, a relatively small example you can use as a base for writing tests and for manual testing and debugging, and
    • this pool-test.c [last updated 2026-04-26], which tries a bunch of patterns for running tasks

    I would recommend modifying pool-example.c to have the tasks take longer (such as by having them call nanosleep) and then use that check that your pool_wait and pool_stop actually wait for the tasks to finish. (pool-test.c might not reliably detect this type of problem.)

    In addition, I would strongly recommend compiling and linking with -fsanitize=thread as part of your testing to enable ThreadSanitizer, which can help identify race conditions more reliably. We will do this as part of our grading.

    Note that, because race conditions depend on timing, tests may not consistently trigger those bugs. In addition to using ThreadSanitizer, you can try to trigger race conditions more consistently by trying a test that might have a race condition many times (for example, in a loop).

    Also, note that these examples/tests don’t check that your programs do not consume a lot of compute time while waiting for an event (and we intend to check that when grading).

  3. Submit your implementation using the submission site.

2 pool.h

#ifndef POOL_H_
#define POOL_H_

// The function signature for a task.
//
// A task should be a function like:
//
//     void *my_task(void*) { ... }
//
typedef void*(*task_fn)(void*);

// Initialize the thread pool.
void pool_setup(int threads);

// Submit a new task to the thread pool and return its ID.
int pool_submit_task(task_fn task, void *argument);

// Get the return value from a task.
void *pool_get_task_result(int task_id);

// Return after all existing tasks have finished.
void pool_wait(void);

// Tell the thread pool threads to finish any current task and then exit.
void pool_stop(void);

#endif

3 Example of pool library usage

Suppose a user wanted to use the thread pool library to handle summing arrays of numbers. They might make a function to do this like:

struct SumInfo {
    int *array;
    int array_size;
};

void *sum_function(void *argument) {
    SumInfo *sum_info = argument;
    int sum = 0;
    for (int i = 0; i < sum_info->array_size; i += 1) {
        sum += sum_info->array[i];
    }
    return (void*) sum;
}

Then arrange for many sums to be computed by the thread pool:

pool_setup(num_threads);

struct SumInfo sum_A_info = { .array = arrayA, .array_size = ARRAY_SIZE };
int task_A = pool_submit_task(sum_function, &sum_A_info);
struct SumInfo sum_B_info = { .array = arrayB, .array_size = ARRAY_SIZE };
int task_B = pool_submit_task(sum_function, &sum_B_info);
...

pool_wait();
int sum_A = (int) pool_get_task_result(task_A)
int sum_B = (int) pool_get_task_result(task_B)
...

pool_stop();

The thread pool will start exactly num_threads threads, regardless of how many arrays there are to sum. Then it will have those threads run array-summing operations until there are no operations left.