Welcome to our next session. In our previous lesson, we built a crucial component for concurrent programming: a thread-safe task queue using POSIX mutexes and condition variables. You now have a solid understanding of how to safely share data between threads and how to make them wait efficiently without burning CPU cycles.
Today, we will take that foundational block and build a complete, high-performance thread pool around it. A thread pool is a core pattern in system programming, designed to manage a fixed group of worker threads that execute tasks concurrently. By the end of this lesson, you will have implemented a general-purpose C thread pool, ready to be integrated into our project to dramatically speed up HNSW graph construction by processing multiple vector insertions in parallel.
Why Not Just Create Threads on Demand?
Your interest in high-frequency trading and performance systems makes this a particularly relevant topic. In such systems, predictability and resource management are paramount. Spawning a new thread for every incoming task—whether it's a market data update or a vector to be inserted—is inefficient and dangerous.
Operating systems incur significant overhead when creating and destroying threads. If tasks arrive in a rapid burst, creating a thread for each one can overwhelm the system, leading to resource exhaustion and a crash.
[PDF] CS3214 Fall 2023 Project 2 - “A Fork-Join Framework”
This handout from a university course on systems programming provides an excellent real-world example of this problem.
Read the Background section. The author presents a C++ example that attempts a parallel sum using std::async. Notice how this seemingly simple approach fails with a Resource temporarily unavailable error. This happens because it blindly tries to spawn thousands of threads, hitting the operating system's limits. This is the exact problem a thread pool is designed to solve.
A thread pool pre-allocates a fixed number of worker threads and reuses them. This amortizes the cost of thread creation over the lifetime of the application, providing a resource-efficient and scalable way to handle a dynamic workload.

The Anatomy of Our Thread Pool
To build our thread pool, we'll need to define a few key data structures. The design needs to be generic enough to handle any kind of task, which is a hallmark of good system library design.
The Task (thread_job)
First, we need a way to represent a task. A task should consist of two parts: the function to execute and the arguments for that function. Using function pointers and void pointers allows us to create a completely generic task structure. For our vector database, one task might be insert_node, while for another application, it could be process_network_packet.
Multithreading with a Thread Pool in C
This video by Eric O Meehan provides a very clear C implementation of a thread pool. We'll follow its structure.
Watch the segment from "what is the thread going to pull". The presenter defines a thread_job struct. Pay close attention to how it uses a function pointer and a void* for the argument. This is the key to making the thread pool versatile.
Our task struct, which we'll call tpool_work, will look something like this:
typedef struct {
void (*func)(void *arg); // Pointer to the function to execute
void *arg; // Pointer to the arguments for the function
} tpool_work_t;
The Pool (thread_pool)
Next, the main thread_pool struct will encapsulate all the components: the threads themselves, the task queue, and the synchronization primitives.
Multithreading with a Thread Pool in C
Let's look at the main struct definition.
Watch the segment from "our struct is going to need" where the threadpool struct is defined. It contains: An array of pthread_t to hold the thread identifiers. The task queue. The mutex and condition variable we implemented in the last lesson. A flag to control the pool's lifecycle (e.g., for a graceful shutdown).
Our pool struct, tpool_t, will integrate the task queue you already built and manage the worker threads:
typedef struct tpool {
tpool_work_t *work_queue; // The task queue (from previous lesson)
size_t work_count;
size_t queue_size;
pthread_t *threads; // Array of worker threads
size_t thread_count;
pthread_mutex_t lock; // Mutex for the queue
pthread_cond_t notify; // Condition variable to signal workers
bool shutdown; // Flag to signal shutdown
} tpool_t;
Implementation: From Blueprint to Reality
Now let's walk through the three main functions required to operate the pool: creation, task submission, and destruction.
1. Creating the Pool (tpool_create)
This function initializes the entire structure. It needs to:
- Allocate memory for the
tpool_tstruct itself. - Allocate memory for the array of
pthread_tworkers. - Initialize the mutex and condition variable.
- Create the threads. This is done in a loop, calling
pthread_createfor each worker.
The most important part is the function we pass to pthread_create. This is the "worker function"—the code that every thread in the pool will execute for its entire lifetime. We also need to pass each thread a pointer to the pool structure so it can access the shared queue and synchronization primitives.
Multithreading with a Thread Pool in C
The constructor function in the video demonstrates these steps clearly.
Watch the segment from "let's actually go in and construct". The code allocates memory for the pool and the thread array, initializes the components, and then enters a for loop to call pthread_create for each thread. Notice how a pointer to the pool itself is passed as the argument to the thread's starting routine.
2. The Worker's Loop
The worker function is the engine of the thread pool. It's a simple, elegant, and powerful loop that dictates the life of a worker thread.
- Enter an infinite loop that only breaks on a shutdown signal.
- Acquire the lock to safely access the task queue.
- Wait for work. This is where the
pthread_cond_waitlogic from our last lesson comes in. The thread will sleep efficiently until the queue is not empty or a shutdown is signaled. Thewhileloop is essential to handle spurious wakeups. - Check for shutdown. If a shutdown is signaled and the queue is empty, the thread should exit.
- Get the task. If work is available, the thread pops a task from the front of the queue.
- Release the lock. This is critical! The lock is released before executing the task. This allows other threads to get work from the queue while this thread is busy, achieving true parallelism.
- Execute the task. The thread calls the function pointer from the task struct, passing it the
void*argument. - Clean up the task struct and loop back to wait for the next job.
Multithreading with a Thread Pool in C
The video's thread_job function is a perfect implementation of this worker loop.
Study this core logic carefully in the section from "what is the thread itself going to do". This is the heart of the thread pool. The presenter explains the flow of locking, waiting, getting a job, unlocking, and executing. This pattern ensures both thread safety and high performance.
3. Submitting a Task (tpool_submit)
This function is what the "client" of the thread pool (e.g., our main thread) calls. Its job is simple:
- Allocate a new task struct.
- Populate it with the function pointer and argument.
- Add the task to the pool's queue.
The push operation on our thread-safe queue will handle locking the mutex, adding the task, and then calling pthread_cond_signal to wake up one of the sleeping worker threads.
4. Destroying the Pool (tpool_destroy)
A graceful shutdown is a non-trivial but essential feature for robust systems programming. We can't just kill the threads; we need to let them finish their current work and exit cleanly.
- Signal shutdown: Set the
shutdownflag in the pool struct. - Wake everyone up: Call
pthread_cond_broadcast. This wakes up all threads waiting on the condition variable. They will see theshutdownflag is true and begin to exit their loops. - Join the threads: Call
pthread_joinfor every thread in the pool. This makes the main thread wait until each worker thread has completely finished execution. This is a crucial synchronization step. - Clean up memory: Once all threads are joined, it is safe to free all dynamically allocated memory: the task queue, the thread array, and the pool struct itself.
Multithreading with a Thread Pool in C
The shutdown logic requires careful ordering of operations to avoid race conditions and deadlocks.
Watch the destructor implementation from "let's talk about the destructor". The presenter explains why you must first signal all threads to exit, and then join them in a separate loop. This two-phase process is a standard and correct way to shut down a thread pool.
Applying the Thread Pool to HNSW Construction
With our thread pool implemented, we can now use it to parallelize the most expensive part of our project so far: building the HNSW graph.
The original, sequential approach would be:
// Sequential
for (int i = 0; i < num_vectors; ++i) {
insert_vector_into_hnsw(graph, &dataset[i]);
}
With the thread pool, the logic changes. The main thread becomes a pure "producer":
// Parallel with Thread Pool
tpool_t* pool = tpool_create(num_cpu_cores);
for (int i = 0; i < num_vectors; ++i) {
// Package data for the task
insertion_args* args = malloc(sizeof(insertion_args));
args->graph = graph;
args->vector = &dataset[i];
// Submit the task to the pool
tpool_submit(pool, insert_vector_into_hnsw, args);
}
// Wait for all tasks to finish (if needed) and destroy the pool
tpool_destroy(pool);
Each call to tpool_submit returns almost instantly. The actual work of finding neighbors and adding connections to the graph is performed concurrently by the worker threads. This will drastically reduce the total time it takes to build the index.
Conclusion
In this lesson, we've assembled a complete and reusable C thread pool, a powerful tool for any performance-sensitive application. You've gone from the core synchronization primitives to a high-level concurrency pattern.
Key Takeaways:
- Thread pools manage a fixed set of worker threads to avoid the high cost of creating and destroying threads for each task.
- The worker thread's main loop is a cycle of locking, waiting for a task (using
pthread_cond_wait), grabbing the task, unlocking, and executing it. - Generic tasks using function pointers and
void*arguments make the thread pool a versatile and reusable component. - A graceful shutdown procedure is critical for correctness, involving setting a flag, broadcasting to all threads, and joining them before freeing resources.
- By decoupling task submission from execution, a thread pool is the perfect tool to parallelize our HNSW graph construction.
In our next lesson, we will address a new challenge. Our thread pool speeds up insertions, but what if we want to search the graph while it's being modified? This introduces new race conditions. We will tackle this by implementing read-write locks (pthread_rwlock_t), which allow for multiple concurrent readers but ensure exclusive access for a writer, further enhancing the performance and utility of our vector database.