Skip to main content

Welcome to a Grasp lesson

Create a free account to keep studying and create your own study path.

Lesson 45

Implementing a Complete Thread Pool

Recommended resources

Hello! In our previous lessons, we've systematically built up the essential components for a thread pool: a worker thread loop that waits efficiently using condition variables, a task submission mechanism that signals workers, and a graceful shutdown protocol.

Today, we will integrate all these pieces into a complete, reusable, and robust thread pool module in C. Your learning outcome is to implement the complete thread pool, including creation, task submission, and shutdown. By the end of this lesson, you will have a header and a source file that constitute a self-contained thread pool library, ready to be used in any C project.

This is a capstone lesson for this module, bringing together theory and practice into a tangible, powerful tool for concurrent programming.

The Thread Pool Architecture

Let's start with a high-level view. A thread pool consists of a fixed number of worker threads that pull tasks from a shared task queue.

Thread Pool Architecture with Blocking Queue
This diagram shows the core concept: a central queue (BlockingQueue) feeds tasks to a set of persistent worker threads. Our implementation will use a linked list protected by a mutex and condition variable to create this blocking queue.

To implement this in a modular way, we'll create:

  1. tpool.h: A header file defining the public API and the opaque tpool_t type. This is what a user of our library sees.
  2. tpool.c: The implementation file containing the definitions of our data structures, the logic for the worker threads, and the implementation of our public API functions.

A Complete Implementation Walkthrough

The most effective way to understand the full implementation is to see it built from scratch. The following video by Eric O Meehan provides a comprehensive, step-by-step construction of a thread pool in C. It covers every detail, from the data structures to the final shutdown sequence.

We will go through this video in sections. I encourage you to code along or, at the very least, pay close attention to the design decisions and implementation details.

Multithreading with a Thread Pool in C

This video, 'Multithreading with a Thread Pool in C', will be our primary guide. We will watch it in segments to analyze each part of the implementation.

For now, just get ready to watch. We will break down the content section by section.

1. Defining the Core Data Structures

First, we need to define the data structures that will manage the pool's state and the tasks themselves.

Multithreading with a Thread Pool in C

Let's start with the foundation. Watch the first part of the video where the threadpool and thread_job structs are defined.

Watch from 01:29 to 08:59. Focus on the members of the threadpool struct. Notice how it contains everything needed to manage the pool: the threads themselves (pthread_t *pool), the task queue (struct q work), synchronization primitives (pthread_mutex_t lock, pthread_cond_t signal), and the shutdown flag (int active).

The thread_job struct is designed for flexibility. By using void *(*job)(void *) for the function pointer and void *arg for the argument, we can enqueue any function that matches this signature. This is a common C pattern for creating generic, reusable components, leveraging your knowledge of pointers to abstract away concrete data types.

2. Creating and Initializing the Pool

The constructor is responsible for allocating memory, initializing all the components of the threadpool struct, and, most importantly, creating the worker threads.

Multithreading with a Thread Pool in C

Now, let's see how the thread pool is brought to life.

Watch from 08:59 to 15:33. Observe how malloc is used to allocate space for the array of pthread_t handles. The key part is the for loop where pthread_create is called for each worker. Notice that a pointer to the pool itself (tm in the video, but let's call it pool) is passed as the argument to each worker's starting function. This gives every worker a reference back to the shared state.

3. The Worker Thread's Execution Loop

This is the heart of the thread pool. Each worker thread runs this function in an infinite loop, performing the cycle of waiting, fetching a task, and executing it.

Multithreading with a Thread Pool in C

This next section implements the logic we've been discussing over the last few lessons. It's the most critical piece of the implementation.

Watch carefully from 15:33 to 27:24. This segment implements the worker's job function. Pay special attention to this sequence inside the loop: pthread_mutex_lock() The while loop around pthread_cond_wait() to handle spurious wakeups. Checking the active flag to allow for shutdown. Retrieving the job from the queue. Crucially, pthread_mutex_unlock() is called before the job is executed. This minimizes the time the lock is held, maximizing concurrency.

Unlocking the mutex before executing the task is a vital optimization. If the lock were held during task execution, the thread pool would effectively become single-threaded, as only one worker could hold the lock and run a task at a time.

4. The Graceful Shutdown

As we covered in the last lesson, a graceful shutdown is non-negotiable. This involves setting a flag, waking all threads, and joining them.

Multithreading with a Thread Pool in C

Finally, let's implement the destructor, which performs the graceful shutdown.

Watch from 30:20 to 36:59. This is a direct implementation of the shutdown protocol from our previous lesson. Note the use of two separate for loops: one to signal all threads to wake up, and a second to join all threads. As we discussed, this separation is essential to prevent deadlocks.

The video uses a loop of pthread_cond_signal to wake the threads. pthread_cond_broadcast would also work and is arguably more idiomatic for this purpose, but the result is the same: all waiting threads are unblocked.


Assembling the Final Module

The video provides a fantastic, albeit monolithic, implementation within a single file. A more robust design separates the public interface from the private implementation. The "Thread Pool in C" blog post by John Nachtimwald provides an excellent example of this modular structure.

Let's define our final API in tpool.h.

tpool.h

#ifndef __TPOOL_H__
#define __TPOOL_H__

#include <stdbool.h>
#include <stddef.h>

// Opaque thread pool object
typedef struct tpool tpool_t;

// The signature for a work function
typedef void (*thread_func_t)(void *arg);

// Creates a thread pool with `num` threads.
tpool_t *tpool_create(size_t num_threads);

// Destroys the thread pool, waiting for currently executing tasks to finish.
void tpool_destroy(tpool_t *pool);

// Adds a new work item to the pool's queue.
bool tpool_add_work(tpool_t *pool, thread_func_t func, void *arg);

// Blocks until all work in the queue is processed.
void tpool_wait(tpool_t *pool);

#endif /* __TPOOL_H__ */

This API is clean and hides the implementation details. The tpool_t is an opaque pointer, so users of the library cannot directly manipulate its internal state, which is a core principle of good API design.

The implementation in tpool.c would then contain the full definitions of struct tpool_work and struct tpool, and all the logic we saw in the video. Helper functions, like the worker function itself, should be declared static to limit their scope to tpool.c.

Thread Pool in C | John's Blog

For a complete, well-structured textual example of this modular approach, I recommend reviewing this blog post. It implements a very similar design and is a great reference for how to structure your final code.

Skim through the entire article. You don't need to read every line in detail, but notice the structure: The clean public API in the header. The internal data structures (tpool_work, tpool). The static worker function (tpool_worker). The implementations for create, destroy, add_work, and wait. This provides a complete picture of a production-quality thread pool module.

Test your understanding!

The tpool_wait function is a useful utility that blocks the calling thread until the pool has no more pending or active tasks. Looking at the tpool.h and tpool.c from the blog post, how is this functionality implemented? Specifically, what state does it check, and what synchronization primitive does it use to wait efficiently?

Show answer

The tpool_wait function is implemented using a second condition variable, working_cond.

  1. State Tracking: The tpool struct maintains a counter, working_cnt, which tracks the number of threads currently executing a task.
  2. Waiting Logic: Inside tpool_wait, the function locks the mutex and enters a while loop. It waits as long as there is work in the queue (work_first != NULL) OR there are threads actively working (working_cnt != 0). The actual wait is done via pthread_cond_wait(&tm->working_cond, ...).
  3. Signaling Logic: The signal for this condition comes from the worker thread. After a worker finishes a task, it decrements working_cnt. If it's the last active thread (working_cnt == 0) and the work queue is also empty, it calls pthread_cond_signal(&tm->working_cond). This signal wakes up the thread that called tpool_wait, which then re-checks the conditions and can finally exit its loop.

Conclusion

Congratulations! You have now walked through the complete implementation of a general-purpose thread pool in C. You have seen how to define the core data structures, create and initialize the pool, implement the worker's execution logic, add a clean public API for submitting tasks, and design a robust, deadlock-free shutdown mechanism.

Key Takeaways:

  • A complete thread pool is a composition of several key components: a thread array, a task queue, a mutex, a condition variable, and a shutdown flag.
  • The implementation should be modular, with a public header file (.h) defining the API and a private source file (.c) containing the logic. Internal functions and data structures should be hidden using the static keyword.
  • The worker thread's loop is the engine of the pool. It must wait efficiently using pthread_cond_wait and minimize lock contention by unlocking the mutex before executing long-running tasks.
  • The shutdown sequence (set flag, broadcast, join) is critical for correctness and must be followed precisely to avoid deadlocks and resource leaks.

Preview of the Next Lesson:
So far, our concurrency control has centered around locks (mutexes). While powerful, locks can introduce performance bottlenecks due to contention and context switching. In the next module, we will explore a different paradigm: lock-free programming. We will start by learning how to use C11 atomic types (_Atomic) to implement a lock-free counter, a fundamental building block for writing highly concurrent, high-performance code without using mutexes at all.