Hello! Welcome back to our course on C threads.
In the previous module, we completed a deep dive into building a high-performance, concurrent hash table. That exercise gave us invaluable experience with fine-grained locking and the complex trade-offs involved in concurrent data structure design. We are now shifting our focus from a specific data structure to a powerful and ubiquitous concurrency pattern: the thread pool.
Today's lesson addresses the first step in building one: designing its core components. By the end of this lesson, you will be able to design the data structures needed for a thread pool, specifically its task queue and the set of worker threads that service it.
Why Use a Thread Pool?
In our past examples, we've often followed a simple "thread-per-task" model: when a piece of work needed to be done concurrently, we spawned a new thread with pthread_create. While straightforward, this model has significant drawbacks in real-world applications:
- High Overhead: Thread creation and destruction are expensive operations for the operating system. If your application needs to perform thousands of small, short-lived tasks, the overhead of creating and tearing down a thread for each one can dominate the actual work being done.
- Resource Exhaustion: An uncontrolled number of threads can consume excessive memory and scheduler resources, potentially slowing down the entire system. At some point, adding more threads leads to performance degradation due to context-switching overhead, a phenomenon known as "thread thrashing."
A thread pool solves these problems by creating a fixed number of worker threads at the start of the program. These threads are then reused to execute multiple tasks throughout the application's lifetime.
Thread Pools in C (using the PTHREAD API)
To start, let's get a high-level overview of what a thread pool is and the problem it solves. This video from CodeVault provides a clear conceptual introduction.
Watch the first 2 minutes and 37 seconds of the video. Focus on the core idea: instead of creating a new thread for every task, we have a fixed set of threads that pull tasks from a shared queue.
Architectural Blueprint
As the video explained, a thread pool consists of a few key components interacting in a producer-consumer pattern.

Let's break down the components we need to design:
- The Task: A representation of a single unit of work.
- The Task Queue: A thread-safe data structure to hold tasks waiting to be executed.
- The Worker Threads: A fixed set of threads that run a loop, pulling tasks from the queue and executing them.
- The Pool Manager: An encapsulating structure that holds everything together—the threads, the queue, and the necessary synchronization primitives.
Now, let's translate this blueprint into C data structures.
1. Designing the Task
A key goal is to create a general-purpose thread pool that can execute any kind of task. This means we can't hardcode the task function or its parameters. Given your background in C, you'll recognize that function pointers are the perfect tool for this. To also handle arguments, we can define a struct that bundles a function pointer with its arguments.
The standard C approach for this kind of generic programming is to use a function pointer that accepts and returns void*, and to pass the arguments as a void* as well.
// A generic representation of a task to be executed.
typedef struct {
void (*function)(void *arg); // Pointer to the function to be executed.
void *argument; // Pointer to the arguments for the function.
} thread_task_t;
This design is incredibly flexible. The function can be any function that takes a single void* argument. If the function needs multiple arguments, they can be bundled together in a separate struct and a pointer to that struct can be passed as the void* argument.
2. Designing the Task Queue
The task queue must be a First-In, First-Out (FIFO) data structure. It also needs to be thread-safe, as multiple worker threads and one or more producer threads will be accessing it concurrently. While you could implement this with a simple array and manage indices, a linked list is often a more flexible choice as it doesn't have a hard-coded capacity.
We can implement the queue directly within our main thread pool structure using pointers to the head and tail of a linked list of tasks.
// Using the task struct from before, which now needs a 'next' pointer.
typedef struct thread_task {
void (*function)(void *arg);
void *argument;
struct thread_task *next; // Pointer to the next task in the queue.
} thread_task_t;
// ... Inside our main thread pool struct ...
// thread_task_t *head;
// thread_task_t *tail;
// pthread_mutex_t queue_mutex;
Here, head points to the next task to be taken by a worker, and tail points to where new tasks will be added. All operations on head and tail must be protected by a mutex (queue_mutex) to prevent race conditions.
Test your understanding!
In the linked-list-based queue design above, why is it beneficial to have pointers to both the head and the tail of the list? What would be the performance implication for enqueueing (adding a task) if we only stored the head pointer?
Show answer
Having pointers to both head and tail makes both enqueue and dequeue operations O(1).
- Dequeue (get work): A worker thread takes the task at
headand updatesheadtohead->next. This is an O(1) operation. - Enqueue (add work): A producer thread adds a new task after
tailand updatestailto point to the new task. This is also an O(1) operation.
If we only stored the head pointer, dequeueing would still be O(1). However, to enqueue a new task, we would have to traverse the entire list from the head to find the last element. This would make the enqueue operation O(n), where n is the current number of tasks in the queue. This could become a significant bottleneck under heavy load.
3. Designing the Worker Threads
The worker threads are the engine of the pool. Their logic is conceptually an infinite loop:
- Lock the queue's mutex.
- Check if there is work in the queue.
- If yes, dequeue a task. Unlock the mutex. Execute the task. Go back to step 1.
- If no, wait for a signal that new work has been added.
- Upon being woken up, go back to step 2.
To manage these threads, we will need an array of pthread_t handles. When we create the thread pool, we will loop N times, calling pthread_create to spawn our N worker threads. Each thread will be started with the same worker function that contains the loop described above.
4. Designing the Main Thread Pool Struct
Finally, we need a single struct to encapsulate all these components. This manager struct acts as the central hub for the entire thread pool.
Thread Pool in C | John's Blog
The blog post 'Thread Pool in C' provides a clear and concise implementation that serves as an excellent reference for our design. Let's examine how it defines the core data structures.
Please read the section titled 'Object data'. Pay close attention to the C code defining struct tpool_work (the task, including the next pointer) and struct tpool (the main pool). Note how the tpool struct encapsulates all the necessary components we've discussed: the linked-list pointers for the queue (work_first, work_last), the synchronization primitives (work_mutex, work_cond), and various state variables (working_cnt, thread_cnt, stop).
Based on that reference, a complete design for our main threadpool_t struct would look something like this:
#include <pthread.h>
// A generic task, with a next pointer for the linked list.
typedef struct thread_task {
void (*function)(void *arg);
void *argument;
struct thread_task *next;
} thread_task_t;
// The main thread pool structure.
typedef struct {
pthread_mutex_t lock; // Mutex to protect internal data structures.
pthread_cond_t notify; // Condition variable to signal worker threads.
pthread_t *threads; // Array of worker threads.
thread_task_t *head; // Head of the task queue.
thread_task_t *tail; // Tail of the task queue.
int thread_count; // Number of threads in the pool.
int task_count; // Number of pending tasks in the queue.
int shutdown; // Flag to signal the pool to shut down.
} threadpool_t;
This structure contains everything needed to manage the pool:
- Synchronization: A mutex (
lock) to protect the task queue and other shared variables, and a condition variable (notify) to allow worker threads to sleep when the queue is empty and be woken efficiently when a new task is added. - State Management: The array of
pthread_thandles, pointers to the queue's head and tail, counts for threads and pending tasks, and a flag for graceful shutdown.
Conclusion
In this lesson, we laid the architectural groundwork for our thread pool. We moved from the high-level concept of reusing threads to a concrete C-level design of the necessary data structures.
Key Takeaways:
- A thread pool avoids the high overhead of the "thread-per-task" model by reusing a fixed set of worker threads.
- The core components are a task representation, a task queue, a set of worker threads, and a manager struct.
- A generic task can be represented by a
structcontaining a function pointer (void (*f)(void*)) and avoid*argument, enabling the pool to execute any type of work. - The task queue is a thread-safe FIFO data structure, often implemented as a linked list protected by a mutex to ensure O(1) enqueue and dequeue operations.
- The main thread pool
structencapsulates all these components, including the necessary synchronization primitives (pthread_mutex_t,pthread_cond_t) and state variables.
Preview of the Next Lesson:
Now that we have designed the data structures, our next step is to bring them to life. In the next lesson, we will implement the worker thread's execution loop. We'll focus on how threads can safely fetch tasks from the queue and, crucially, how they can wait efficiently using condition variables when the queue is empty, avoiding the CPU-burning pitfall of busy-waiting.