Concurrency
Thread Pools
Build a thread pool using worker loops, task queues, and condition variables to optimize resource usage.
Interview: Why thread creation is slow, how thread pools optimize resource usage, and implementing task queues.
Spawning a new thread introduces system overhead due to OS context allocation. A Thread Pool resolves this by pre-allocating a fixed set of worker threads that pull tasks from a shared queue.
Task Queue
A queue containing pending tasks (typically stored as std::function<void()>) protected by a mutex.
Worker Loop
Threads sleep on a condition variable, waking up to pull and execute tasks from the queue before sleeping again.
Core Matching
Size the thread pool dynamically using std::thread::hardware_concurrency() to match the target CPU.
Thread Pool Architecture
Thread pools use condition variables to manage workers:
- The main thread pushes a task into the queue and notifies a worker using
notify_one(). - An idle worker thread wakes up, locks the queue, extracts the task, unlocks the queue, and executes the task.
- On destruction, the pool sets a stop flag, notifies all worker threads, and joins them.
Code Walkthrough
A basic implementation of a thread pool using standard C++ features.
#include <iostream> #include <vector> #include <queue> #include <thread> #include <mutex> #include <condition_variable> #include <functional>class ThreadPool { private: std::vector<std::thread> m_workers; std::queue<std::function<void()>> m_tasks; std::mutex m_queueMutex; std::condition_variable m_cv; bool m_stop = false;
public: ThreadPool(size_t threads) { for (size_t i = 0; i < threads; ++i) { m_workers.emplace_back([this] { while (true) { std::function<void()> task; { std::unique_lock<std::mutex> lock(this->m_queueMutex); this->m_cv.wait(lock, [this] { return this->m_stop || !this->m_tasks.empty(); });
if (this->m_stop && this->m_tasks.empty()) return;
task = std::move(this->m_tasks.front()); this->m_tasks.pop(); } task(); // Execute task outside lock } }); } }
void enqueue(std::function<void()> task) { { std::lock_guard<std::mutex> lock(m_queueMutex); m_tasks.push(task); } m_cv.notify_one(); }
~ThreadPool() { { std::lock_guard<std::mutex> lock(m_queueMutex); m_stop = true; } m_cv.notify_all(); for (std::thread& worker : m_workers) { if (worker.joinable()) worker.join(); } } };
int main() { ThreadPool pool(4); for (int i = 0; i < 8; ++i) { pool.enqueue([i] { std::cout << "Task " << i << " executed on thread: " << std::this_thread::get_id() << "\n"; }); } return 0; }
Interview-Relevant Information
Q: How does a thread pool optimize system resources?
Answer: A thread pool avoids the overhead of spawning and destroying threads for short-lived tasks by reusing a fixed set of threads. It also prevents thrashing (performance degradation caused by the CPU switching between too many active threads) by matching the thread count to the system's CPU cores.
Q: Why should worker tasks be executed outside the task queue mutex lock?
Answer: Pushing or popping from the task queue is fast. Executing the task itself can take a long time. If the worker thread keeps the mutex locked during task execution, other worker threads cannot access the queue, turning the thread pool into a single-threaded bottleneck.
Quick Checklist
Did you run tasks outside the queue lock scope? Did you join all threads on pool destruction? If yes, your thread pool design is correct.
Use Cases
Handling concurrent connection requests in web servers (like Nginx).
Running parallel tasks (like physics updates and pathfinding calculations) in game engines.
Common Mistakes
Hardcoding the worker thread count instead of checking hardware limits using std::thread::hardware_concurrency().
Failing to notify worker threads on destruction, leaving the pool waiting and causing deadlocks during shutdown.