threadpool is a 300 line C++11 pool that blocks its producers
based on C++11 , a mini threadpool , accept variable number of parameters 基于C++11的线程池,简洁且可以带任意多的参数
At a glance
- What is it?
- lzpong/threadpool is a MIT licensed header-only C++11 thread pool that accepts variadic task arguments, returns a future for the result, and applies backpressure by blocking commit when the queue is full. It suits small projects that want a pool without a framework, and it is two files with a documented deadlock constraint.
- Who is it for?
- Adopt threadpool if you want a C++11 thread pool in a single header with no dependencies, and you can live with a bounded queue that blocks the producer when full. Do not adopt it if you need a pool that grows without a hard thread cap, that supports task cancellation, or that lets a task wait on a subtask in the same pool, because that case is documented as a deadlock.
- Can I use it commercially?
- Yes. MIT is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
- Is it still maintained?
- Yes. The repository last received commits 65 days ago.
- What is it written in?
- Mainly C++, according to GitHub's language statistics.
Answers come from the project's GitHub data, last synced on October 23, 2026, and from our analysis. They are not legal advice.
Editorial analysis
Two files, one header, and a queue that blocks when it is full
threadpool is a mini thread pool based on C++11, and the repository is exactly two source files. There is threadpool.h and Main.cpp, plus the README and the LICENSE. That is the entire project, and the README states the size in the same breath as the feature list: a C++11 thread pool whose code is concise, only about 300 lines.
The mechanism is the classic one. The pool manages a task queue and a thread queue, and each time it takes one task from the queue and assigns it to a thread to execute, over and over. The README notes that when the limit is set to create only one thread, it becomes a complete task queue, which is a nice observation about how the same structure covers both use cases.
What makes it more than a textbook pool is the queue discipline. The features list names a bounded task queue with backpressure control, where commit blocks the producer when the queue is full, preventing unbounded task growth. That is the single most consequential design decision in the library, and it is the one that most separates it from the many minimal thread pools on the internet.
The constructor signature shows the two knobs:
threadpool(size = 4, maxQueueSize = 1024)The size is the initial number of threads and must be at least 1, and it will not exceed the upper limit THREADPOOL_MAX_NUM, which defaults to 16 and can be changed by defining it before including the header. The maxQueueSize is the maximum task queue capacity, forced to 2 if it is less than 2, and when the queue is full commit blocks as the backpressure mechanism.
The reason to prefer blocking over dropping or growing is that both alternatives lose work or lose predictability. A dropped task is a silent bug, and an unbounded queue is a memory leak with extra steps. Blocking makes the producer pay the cost, which is the honest outcome when the consumer is genuinely slower than the producer.
Variadic commit returns a future, and a class member needs an adapter
The API surface is small and the variadic part is the headline feature. The description says it supports submitting variadic functions or lambda expressions, and that you can get the execution return value through a future.
The methods are these. auto commit(f, args...) -> std::future<Ret> submits a task and returns a future for the result. void commit2(f) submits a task with no return value. int idlCount() returns the number of idle threads, int thrCount() the number of threads, and unsigned int queueSize() the length of the task queue. All three counters are documented as instantaneous snapshots for reference only, which is the honest way to describe a counter read without a lock held across a consistent view.
The variadic commit takes the function first and then its arguments, so the first parameter is f and the rest are the arguments to f. The README makes a specific warning about this: if you need to pass a struct or class, it is recommended to use a pointer, and be careful about the variable's scope. That warning is the price of the convenience, since a variadic template binds by value and the argument lifetime ends when commit returns.
Class member functions are the other sharp edge. The features list says the pool supports class static member functions and global functions, but does not directly support class member functions, and gives two ways to adapt them: bind or mem_fn.
.commit(std::bind(&Dog::sayHello, &dog)).commit(std::mem_fn(&Dog::sayHello), this)The bind form takes the object as a bound argument, and the mem_fn form takes the pointer and passes it as the first argument to the member function. Both work, and the mem_fn form is the more modern choice, but neither is as direct as a plain function call, which is the price of the type erasure behind the queue.
Auto growth is opt-in, and the hard thread cap always applies
The README carries a note that the project has been refactored and optimised, in particular for the self-growing thread pool, where the automatic expansion and the idle thread reclamation were improved.
Auto growth is off by default. The feature list says the pool supports automatic expansion and idle thread reclamation to avoid a large number of idle threads after a peak, and that it is enabled by defining THREADPOOL_AUTO_GROW before including the header, while the maximum number of threads is still limited by THREADPOOL_MAX_NUM.
That combination is the interesting part. Auto growth without a cap is how a thread pool takes a machine down, so the design keeps a hard ceiling that applies whether growth is enabled or not. The default ceiling is 16, and you raise it by defining THREADPOOL_MAX_NUM to 128 or whatever you need before including the header.
The idle thread reclamation is the other half of the feature. A pool that grew to 64 threads during a spike and kept them afterwards is worse than one that never grew, because the threads sit there consuming memory and scheduler attention for the rest of the process's life. Reclaiming them means the pool returns to its baseline size once the burst is over, which is what makes growth safe to enable at all.
The counters exist to let you observe this. idlCount() and thrCount() give you the idle and total counts at a point in time, and queueSize() gives the queue depth, and the combination is enough to tell whether the pool is saturated, which is the state in which commit will block and your producer will stall.
Because the counters are snapshots rather than consistent views, they are for dashboards and for debugging a stuck producer, not for making control decisions. The README's own wording, 仅参考, is the right level of caution.
Three usage constraints, one of which is a deadlock
The constraints section is short and each one is a real footgun.
The first is a deadlock. It is forbidden to synchronously wait inside a task for a subtask submitted to the same pool, because that will deadlock. The reason is structural. The pool has a bounded number of threads, and if every thread is blocked waiting on a future that can only be completed by another thread in the same pool, no thread is left to run the queued work.
This is the classic thread pool deadlock and it is the one that bites people, because the code that triggers it looks perfectly reasonable. A task that fans out into parallel subtasks and then waits for them is the natural way to write it, and it is exactly the pattern that hangs.
The second constraint is about the destructor. During destruction, commit and commit2 must not be called concurrently. The pool's shutdown joins its threads, and a commit arriving during that window has no defined behaviour. The destructor itself is safe, because the README notes that join() can wait for all tasks to finish before ending, which is very safe.
The third restates the backpressure: when the queue is full, commit blocks, to prevent unbounded growth.
Exception safety is covered separately in the feature list, and it is a genuine strength. If construction fails, the already created threads are cleaned up automatically. A task that throws does not affect the thread pool, and the exception is propagated to the caller through the future. That last part is the important design point, because without it a task exception would terminate the process in a thread, which is the default behaviour for an uncaught exception in a std::thread.
The combination of a variadic interface, futures for results, exception propagation and a bounded queue is closer to a small job system than to the minimal pools that the same size usually implies, and that is what makes 300 lines a reasonable target.
The README teaches the C++11 features the implementation relies on
One of the more unusual things about this project is that the README spends a long section explaining the C++11 features the code uses, on the premise that understanding the principle is not the same as being able to write the code.
The list runs to sixteen items and it is a genuinely useful primer on the specific constructs in the header. There is the Task type alias, which is std::function<void()>, accepting any void-returning function prototype, a function object or an anonymous function. There is the emplace_back versus push_back distinction for the thread vector, where emplace_back constructs in place and is faster. There is the choice of {} initialisation over () everywhere, described as more consistent and less error prone. There is the capture clause, with this capturing the this pointer, and the lambda that runs an internal infinite loop using _task_cv.wait(lock, [this]{...}) to block the thread.
Then the type machinery: decltype(expr) infers the type of expr, and auto f(A a, B b) -> decltype(a+b) is one usage, which cannot be written the other way round because that is the rule.
The rest covers the concurrent toolkit. lock_guard is the RAII wrapper over a mutex that locks on construction and unlocks on destruction. A condition_variable needs a unique_lock, whose advantage over lock_guard is that it can unlock and lock at will, and cv.wait() requires the mutex to be held, with wait itself unlocking it and reacquiring it when the predicate is satisfied. packaged_task is the task wrapper that exposes get_future(), and the future is what carries the return value. make_shared constructs a shared_ptr. bind curries a function with some arguments. forward() preserves the value category of an argument where move() rightifies it. And deque with front, pop_front, back and push_back is the queue type underneath.
The register of the section is worth noting: the C++11 language details heading opens by saying that even if you understand the principle it does not mean you can write the program, and then the explanation of the commit method being a bit odd ends with the observation that the rules are the rules. The author is writing for a Chinese-speaking audience of C++ developers, and the English README translation preserves that voice.
One compile line, and an honest note about where the idea came from
Building it is a single command on Linux, and the README gives the compile line and the run line together:
$ g++ Main.cpp -o main -lpthread # 编译方式
$ ./threadpool # 执行The -lpthread is the only external requirement, and it comes from the fact that the pool is built on std::thread and std::condition_variable.
On Windows, with Visual Studio or MinGW, you compile Main.cpp directly, and the code is already compatible with _WIN32. The portability work is in the header, and the README notes it as a fact rather than as a feature, which is the right register for something that a header-only library has to do to be usable at all.
The copyright section is unusually candid. The code is written by the author, but the idea comes from the internet, in particular from the ThreadPool implementation by progschj, which was basically copied, together with another implementation and its explanation from a blog post, and then modified and made more concise. The phrase used is that good things are worth copying.
The licence is MIT, with the LICENSE file in the repository as the authoritative text, and this is a licensing statement rather than legal advice. The MIT grant covers the header as it now stands, and the acknowledgement in the README is the right way to discharge the attribution obligation for the parts that came from elsewhere.
The repository also has no GitHub releases, and the default branch is master. The last push was on 2026-08-19, and the repository is not archived. The homepage points to a blog post on cnblogs, which is where the original write-up of this implementation lives.
What this amounts to is a well-explained, well-constrained, single-header thread pool. Its two distinguishing features against the many minimal pools it resembles are the backpressure on a bounded queue and the documented constraints that tell you when not to use it, and for a small project that is exactly the right set of things to have.
Editorial conclusion
Adopt threadpool if you want a C++11 thread pool in a single header with no dependencies, and you can live with a bounded queue that blocks the producer when full. Do not adopt it if you need a pool that grows without a hard thread cap, that supports task cancellation, or that lets a task wait on a subtask in the same pool, because that case is documented as a deadlock. Before you use it, decide whether to enable THREADPOOL_AUTO_GROW, since it is off by default and the maximum thread count is capped by THREADPOOL_MAX_NUM either way.
Frequently asked questions
How do I compile the lzpong threadpool example?
On Linux, run g++ Main.cpp -o main -lpthread and then ./threadpool. On Windows with Visual Studio or MinGW you compile Main.cpp directly, and the code is already compatible with _WIN32.
What happens when the threadpool task queue is full?
commit blocks the producer, which is the backpressure control. The queue is bounded by maxQueueSize, which is forced to 2 if given less than 2, and blocking the producer prevents the task queue from growing without bound.
Can I call a class member function with the lzpong threadpool?
Not directly. The pool supports class static member functions and global functions, but a non-static member function needs an adapter, either with bind as in .commit(std::bind(&Dog::sayHello, &dog)) or with mem_fn as in .commit(std::mem_fn(&Dog::sayHello), this).
How do I enable automatic thread growth in the lzpong threadpool?
Define THREADPOOL_AUTO_GROW before including the header. It is off by default. The maximum number of threads is still limited by THREADPOOL_MAX_NUM, which defaults to 16 and can be changed by defining it before including the header as well.
What are the usage constraints of the lzpong threadpool?
Three: you must not synchronously wait inside a task for a subtask submitted to the same pool, because that deadlocks; you must not call commit or commit2 concurrently during destruction; and commit blocks when the queue is full. Task exceptions do not affect the pool and are propagated to the caller through the future.
Official sources
Add this badge to your README
If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.
[](https://hysenlabs.com/projects/lzpong-threadpool)Was this useful?