Hello! Welcome back to our course on building a thread pool in C.
In our last lesson, we implemented the heart of our thread pool: the worker thread's execution loop. We now have "consumer" threads that can efficiently wait for tasks using pthread_cond_wait, avoiding the pitfalls of busy-waiting.
Today, we'll implement the other side of this producer-consumer relationship. This lesson focuses on creating the public-facing function that allows a client to submit new work to the pool. You will learn how to safely add a task to the shared queue and, crucially, how to signal a waiting worker thread to wake up and process it.
The Producer-Consumer Model
A thread pool is a classic example of the producer-consumer problem.
- Producers create work and add it to a shared buffer (our task queue).
- Consumers take work from the buffer and process it.
In our case, the worker threads are the consumers. The "producer" is any part of your application that calls the function to submit a task—most commonly, the main thread.
The diagram below illustrates this flow. The Main Thread (producer) submits a task, which goes into the Waiting Queue. A Worker Thread (consumer) then takes a task from the queue to execute.

The Logic of Submitting a Task
To implement our threadpool_add_task function, we need to perform a sequence of operations very carefully to ensure thread safety and correctness. The core logic involves these five steps:
- Allocate and Initialize a Task: Dynamically allocate memory for a
thread_task_tstruct and populate it with the function pointer and argument provided by the caller. - Acquire the Lock: Before accessing the shared task queue, we must lock the pool's mutex (
pthread_mutex_lock) to prevent race conditions. - Add the Task to the Queue: Append the newly created task to the end of our linked list queue.
- Signal a Waiting Worker: After the task is successfully added, we must wake up one of the sleeping worker threads. This is done by calling
pthread_cond_signal. - Release the Lock: Unlock the mutex (
pthread_mutex_unlock) to allow other threads (either producers or consumers) to access the queue.
Let's see how this is implemented in practice.
Thread Pool in C | John's Blog
The article 'Thread Pool in C' by John's Blog, which we used in the last lesson, provides a robust implementation of the function for adding work. It clearly shows the steps of creating a work item, locking, adding to the queue, signaling, and unlocking.
Please read the section titled 'Adding work to the queue' and study the tpool_add_work function. Notice how it handles adding the work object to the linked list, distinguishing between an empty list and a non-empty one. Also, pay close attention to the use of pthread_cond_broadcast.
Deconstructing the Task Submission Function
Let's integrate the logic from the reading into our own threadpool_t structure. The implementation will look like this:
// Our structs for context (from previous lessons)
typedef struct thread_task {
void (*function)(void*);
void *argument;
struct thread_task *next;
} thread_task_t;
typedef struct threadpool {
pthread_mutex_t lock;
pthread_cond_t notify;
pthread_t *threads;
thread_task_t *head;
thread_task_t *tail;
int thread_count;
int task_count;
int shutdown;
} threadpool_t;
// The function to add a task
int threadpool_add_task(threadpool_t *pool, void (*function)(void*), void *argument) {
if (pool == NULL || function == NULL) {
return -1; // Invalid arguments
}
// 1. Allocate a new task structure
thread_task_t *task = (thread_task_t *)malloc(sizeof(thread_task_t));
if (task == NULL) {
return -1; // Malloc failed
}
task->function = function;
task->argument = argument;
task->next = NULL;
// 2. Lock the mutex to protect the queue
pthread_mutex_lock(&(pool->lock));
// 3. Add the task to the end of the queue
if (pool->task_count == 0) {
pool->head = task;
pool->tail = task;
} else {
pool->tail->next = task;
pool->tail = task;
}
pool->task_count++;
// 4. Signal a worker thread that a new task is available
pthread_cond_signal(&(pool->notify));
// 5. Unlock the mutex
pthread_mutex_unlock(&(pool->lock));
return 0; // Success
}
This function is the public API for submitting work. It encapsulates all the necessary synchronization, making it safe to be called by multiple producer threads simultaneously.
Signaling: pthread_cond_signal vs. pthread_cond_broadcast
In our code, we used pthread_cond_signal, but the "John's Blog" article used pthread_cond_broadcast. What's the difference?
pthread_cond_signal: Wakes up at least one thread that is currently waiting on the condition variable. If no threads are waiting, the signal is lost.pthread_cond_broadcast: Wakes up all threads that are currently waiting on the condition variable.
When adding a single task, pthread_cond_signal is generally more efficient. It wakes one worker to handle the one new task. Using pthread_cond_broadcast would wake up all idle workers, who would then all contend for the mutex lock. Only one would succeed in taking the task, and the rest would find the queue empty and go back to sleep. This unnecessary contention is known as the "thundering herd" problem.
So why might one use broadcast? It can be simpler and is sometimes necessary if a state change might satisfy multiple threads (e.g., adding a batch of 10 tasks, or signaling a pool-wide shutdown). For our one-task-at-a-time submission, signal is the better choice.
Let's watch a video that builds this submission logic from the ground up.
Thread Pools in C (using the PTHREAD API)
The CodeVault channel has an excellent video that walks through implementing the submit_task function. It starts with the naive implementation and then adds the necessary synchronization, which is a great way to see why each piece is necessary.
Watch the video from 13:30 to 14:52, and then from 19:21 to 20:11. The first part (13:30 - 14:52) shows the core logic of adding a task and protecting the queue with a mutex. The second part (19:21 - 20:11) adds the crucial pthread_cond_signal call to wake up a worker. Notice how this completes the interaction with the pthread_cond_wait we implemented in the worker loop last lesson.
Test your understanding!
In our threadpool_add_task function, the pthread_cond_signal call happens before pthread_mutex_unlock. Is it valid to swap their order, unlocking the mutex first and then signaling? What might be the performance implication of doing so?
Show answer
Yes, it is perfectly valid and often recommended to unlock the mutex before signaling.
// ... inside threadpool_add_task ...
pool->task_count++;
// Unlock first...
pthread_mutex_unlock(&(pool->lock));
// ...then signal.
pthread_cond_signal(&(pool->notify));
return 0;
The "Build Your Own Redis" resource you'll see next actually advocates for this pattern. The performance implication is generally positive. If you signal before unlocking, the woken worker thread immediately tries to acquire the lock but will be blocked until the producer thread calls pthread_mutex_unlock. By unlocking first, you eliminate this immediate contention. The woken worker can acquire the lock right away without having to wait for the producer to release it. This can lead to slightly lower latency and better throughput under high contention.
Finally, let's look at one more resource that provides a very clear, concise pseudo-code and C implementation of the producer side.
14. Thread Pool | Build Your Own Redis with C/C++
The 'Build Your Own Redis' guide has a chapter on thread pools that does a fantastic job of explaining the producer side of the producer-consumer problem. It covers the theory and then provides a compact C implementation.
First, review the section 'Condition variable' (part of 14.3) to reinforce how produce() and consume() interact with the mutex and condition variable. Then, read section 14.5 up to and including 'The producer (event loop)'. The C code for thread_pool_queue is a perfect, minimal implementation of what we are building.
Conclusion
Congratulations! You have now implemented both sides of the producer-consumer pattern for our thread pool. The worker threads ("consumers") can wait for work, and now you have a safe and efficient threadpool_add_task function for the "producers" to submit work.
Key Takeaways:
- Submitting a task is the "producer" role in the producer-consumer pattern.
- The five steps for task submission are: allocate task, lock mutex, add to queue, signal worker, and unlock mutex.
pthread_cond_signalis used to wake up a single sleeping worker thread after a task is added.- For adding single tasks,
pthread_cond_signalis generally more efficient thanpthread_cond_broadcastas it avoids the "thundering herd" problem. - Unlocking the mutex before signaling is a valid and often preferred optimization to reduce lock contention.
Preview of the Next Lesson:
Our thread pool can be created, can execute tasks in parallel, and can have new tasks submitted to it. The last piece of essential functionality is a graceful shutdown. In the next lesson, we will design and implement the threadpool_destroy function, which will ensure that all pending tasks are completed before the threads are joined and all resources are deallocated.