C++
multi threading
thread reuse
job scheduling
concurrency

C multi threading how do I reuse threads for many jobs?

Master System Design with Codemia

Enhance your system design skills with over 120 practice problems, detailed solutions, and hands-on exercises.

Introduction

If you have many small jobs, creating a brand-new thread for each one is usually the wrong design. Thread creation and teardown add overhead, increase scheduler churn, and make shutdown logic more complicated. The usual fix is a thread pool: create a fixed set of worker threads once, queue jobs, and let workers reuse the same threads over and over.

That is what "reusing threads" means in practice. You are not moving one thread object between tasks manually. You are keeping worker threads alive and feeding them new jobs from a shared queue.

Build a Basic Thread Pool

A minimal pool needs four ingredients:

  • a queue of pending jobs
  • a mutex to protect the queue
  • a condition variable so idle workers can sleep
  • a stop flag for clean shutdown

Here is a small C++ example that returns futures so callers can wait for results:

cpp
1#include <condition_variable>
2#include <functional>
3#include <future>
4#include <iostream>
5#include <mutex>
6#include <queue>
7#include <thread>
8#include <vector>
9
10class ThreadPool {
11public:
12    explicit ThreadPool(std::size_t threadCount) : stopping(false) {
13        for (std::size_t i = 0; i < threadCount; ++i) {
14            workers.emplace_back([this] {
15                while (true) {
16                    std::function<void()> job;
17
18                    {
19                        std::unique_lock<std::mutex> lock(mutex);
20                        condition.wait(lock, [this] {
21                            return stopping || !jobs.empty();
22                        });
23
24                        if (stopping && jobs.empty()) {
25                            return;
26                        }
27
28                        job = std::move(jobs.front());
29                        jobs.pop();
30                    }
31
32                    job();
33                }
34            });
35        }
36    }
37
38    template <typename Fn>
39    auto submit(Fn fn) -> std::future<decltype(fn())> {
40        using Result = decltype(fn());
41
42        auto task = std::make_shared<std::packaged_task<Result()>>(std::move(fn));
43        std::future<Result> result = task->get_future();
44
45        {
46            std::lock_guard<std::mutex> lock(mutex);
47            jobs.push([task] { (*task)(); });
48        }
49
50        condition.notify_one();
51        return result;
52    }
53
54    ~ThreadPool() {
55        {
56            std::lock_guard<std::mutex> lock(mutex);
57            stopping = true;
58        }
59
60        condition.notify_all();
61
62        for (std::thread &worker : workers) {
63            worker.join();
64        }
65    }
66
67private:
68    std::vector<std::thread> workers;
69    std::queue<std::function<void()>> jobs;
70    std::mutex mutex;
71    std::condition_variable condition;
72    bool stopping;
73};
74
75int main() {
76    ThreadPool pool(3);
77
78    auto a = pool.submit([] { return 2 + 3; });
79    auto b = pool.submit([] { return 10 * 4; });
80
81    std::cout << a.get() << "\n";
82    std::cout << b.get() << "\n";
83}

The same worker threads execute both jobs. No new threads are created for each submission.

Why a Pool Works Better Than Ad Hoc Threads

Thread reuse matters most when jobs are frequent and short. In that situation, thread creation overhead can dominate the useful work. A pool amortizes that cost by paying it once at startup and then reusing the workers many times.

It also gives you a natural place to control concurrency. If the pool has four workers, only four jobs run at once even if a producer submits hundreds. That bound makes performance and resource usage easier to reason about.

Size the Pool for the Workload

The right number of workers depends on what the jobs do. CPU-bound jobs usually want a pool near the number of hardware cores. I/O-bound jobs can sometimes tolerate more threads because workers spend time blocked on disk or network waits.

More threads are not automatically better. Too many workers increase context switching, cache disruption, and memory usage. Thread pools help because they make concurrency explicit and bounded, not because they create as many threads as possible.

Common Pitfalls

The biggest mistake is creating detached or short-lived threads for every job. That pattern often works in prototypes and then performs poorly as job volume increases.

Another issue is ignoring queue growth. A pool limits running jobs, but it does not automatically limit queued jobs. If producers are faster than consumers, memory usage can still grow without bound.

Shutdown is also easy to get wrong. Workers need a clear stop signal and a wake-up path so they can exit cleanly instead of hanging forever on an empty queue.

Finally, a thread pool does not eliminate synchronization concerns inside the jobs. Shared state accessed by multiple tasks still needs correct locking or another safe coordination strategy.

Summary

  • Reuse threads by keeping a fixed worker pool alive.
  • Push jobs into a queue and wake workers with a condition variable.
  • Return futures or another result handle when callers need outputs.
  • Size the pool based on CPU-bound versus I/O-bound behavior.
  • Add queue limits and clean shutdown logic for production use.

Course illustration
Course illustration

All Rights Reserved.