# dp::thread-pool: a header only C++20 thread pool with three ways to enqueue

> thread-pool is a single header library that gives you a fixed size pool of C++20 threads, with three enqueue variants covering fire and forget, a result you can wait on, and a batch you wait for at the end. The interesting part is the benchmark table, which is honest about where it loses.

**ptsouchlos/thread-pool** — A modern, fast, lightweight thread pool library based on C++2x

- Repository: https://github.com/ptsouchlos/thread-pool
- Website: https://DeveloperPaul123.github.io/thread-pool
- Stars: 570 · Forks: 40
- Language: C++
- License: MIT
- Published: 2026-10-22 · Updated: 2026-10-22 · Language: en
- Canonical page: https://hysenlabs.com/projects/ptsouchlos-thread-pool

## One header, one namespace, three enqueue styles

thread-pool is a simple, fast and functional thread pool implementation using pure C++20, and the library is named dp::thread-pool. The feature list is short: it is built entirely with C++20, it lets you enqueue tasks with or without tracking results, and it points at the high performance section for the numbers.

The API is where the design shows. There are three enqueue variants and they differ in what they give back.

Enqueue tasks without a returned result uses enqueue_detach. You create a pool with a number of threads and push tasks in:

```cpp
// create a thread pool with a specified number of threads.
dp::thread_pool pool(4);

// add tasks, in this case without caring about results of individual tasks
pool.enqueue_detach([](int value) { /*...your task...*/ }, 34);
pool.enqueue_detach([](int value) { /*...your task...*/ }, 37);
pool.enqueue_detach([](int value) { /*...your task...*/ }, 38);
// and so on..
```

Enqueue tasks with a returned value uses enqueue, which returns a handle you can wait on:

```cpp
// create a thread pool with a specified number of threads.
dp::thread_pool pool(4);

auto result = pool.enqueue([](int value) -> int { /*...your task...*/ return value; }, 34);
// get the result, this will block the current thread until the task is complete
auto value = result.get();
```

And enqueue tasks and wait for them to complete is the batch mode, where you fire off a set of detached tasks and then wait once at the end:

```cpp
dp::thread_pool pool(4);

// add tasks, in this case without caring about results of individual tasks
pool.enqueue_detach([](int value) { /*...your task...*/ }, 34);
pool.enqueue_detach([](int value) { /*...your task...*/ }, 37);
pool.enqueue_detach([](int value) { /*...your task...*/ }, 38);
pool.enqueue_detach([](int value) { /*...your task...*/ }, 40);

// wait for all tasks to complete
pool.wait_for_tasks();
```

The three together cover the whole range. Detach for work you never look at again, enqueue for one value you need, and wait_for_tasks for a batch whose completion matters as a group.

## Header only, with a vcpkg package and a CMake target

The integration story is short because the library is header only. All the files needed are in include/thread_pool, so vendoring the directory is the whole procedure. There is nothing to build and nothing to link beyond your own standard library.

For users of package managers, dp::thread-pool is available on vcpkg:

```powershell
vcpkg install dp-thread-pool
```

For CMake users, thread-pool defines the CMake target dp::thread-pool, and find_package works:

```cmake
find_package(thread-pool REQUIRED)
target_link_libraries(your_target PRIVATE dp::thread-pool)
```

The alternative to find_package is to pull the source in directly, and the README suggests CPM, which is based on CMake's Fetch_Content module:

```cmake
CPMAddPackage(
  NAME thread-pool
  GITHUB_REPOSITORY ptsouchlos/thread-pool
  GIT_TAG 0.7.0 # change this to latest commit or release tag
  OPTIONS
    "TP_BUILD_TESTS OFF"
    "TP_BUILD_BENCHMARKS OFF"
    "TP_BUILD_EXAMPLES OFF"  
)
```

The three TP_BUILD options are worth noticing, because they tell you the project is a full repository with tests, benchmarks and examples that you can switch off when you only want the header. The GIT_TAG is pinned to 0.7.0 with a comment saying to change it to the latest commit or release tag, which is the usual caveat for a vendored dependency.

There is one naming wrinkle to be aware of, and the README flags it in a note at the top: the GitHub account for the project was renamed from DeveloperPaul123 to ptsouchlos, with repository ownership and history unchanged. The vcpkg port name, the CMake package name and the GitHub repository name therefore do not all match, and the old name still appears in the documentation site URL.

## The benchmark summary admits where it loses

The benchmark section compares dp::thread_pool against three other libraries: ConorWilliams/Threadpool, bshoshhany/thread-pool, which is C++17, and alugowski/task-thread-pool, which is C++11 with no work stealing. The tests use matrix multiplication with nanobench, and each library is measured against std::function as the baseline, with relative measurements in percent.

The machine was an AMD Ryzen 7 5800X with 16 CPUs at 3800 MHz and 32 GB of RAM.

What the summary says is more interesting than a straight win. In general dp::thread_pool is faster than other thread pool libraries in most cases, and this is especially the case when std::move_only_function is available. fu2::unique_function is a close second, and std::function is the slowest when used inside dp::thread_pool.

Then the two concessions. In certain situations riften::ThreadPool pulls ahead in performance, and the likely reason given is that this library uses a lock-free queue, along with a custom semaphore and a difference in how work stealing is handled. And task_thread_pool seems to pull ahead with large numbers of smaller tasks.

Both of those are structural, not incidental. A fixed pool with a shared queue and no work stealing will lose to a work-stealing pool when tasks are uneven in cost, and it will lose to a lock-free queue implementation when the enqueue rate is the bottleneck. A library that publishes this in its own README is telling you exactly which workloads to avoid, which is more useful than a table that claims a uniform win.

## The detailed table, and the methodology caveats

The detailed results are given as a portion of the MSVC data, with columns for relative percentage, milliseconds per operation, operations per second, error percentage, total, and which comparison the row represents, on a 256x256 matrix multiplication.

The relative column is the interesting one. dp::thread_pool with std::function sits at 100.0 percent, which is the baseline. With std::move_only_function it is 102.9 percent, with fu2::unique_function 98.7 percent. The competitors are BS::thread_pool at 93.5 percent, task_thread_pool at 102.2 percent, and riften::Thiefpool at 100.1 percent, so all of them are within about ten percent of the baseline and two of the three are ahead of it.

The methodology notes are the part that lets you judge the table. Matrix sizes are all square. Each multiplication is (MxM) times (MxM). The benchmarks were run on Windows, so system stability is something to consider, with dynamic CPU frequency scaling given as the example. And the full results are published separately for MSVC and for Clang, in the benchmark results folder, with links to both.

So the claim is more modest than a benchmark section usually makes. On this machine, on this workload, with this compiler, the differences are small, and the choice of callable type moves the number more than the choice of library. That is a useful conclusion for anyone choosing between them.

## Building it and what the toolchain expects

The building section lists what the project has been built with: Visual Studio 2022, Clang 10 and above via WSL on Windows, GCC 11 and above, also via WSL, and CMake 3.25 or newer. The README then gives the build command as cmake -S . -B build followed by a further step, and the section is cut off in the version available here.

The continuous integration badges at the top show the same spread, with separate workflows for Ubuntu, for Windows, for style checking and for an install job, plus a build badge and a codecov badge. So the library is compiled on both major platforms and both major toolchain families, which is what you would want from a header that ends up in other people's builds.

The repository layout is a conventional CMake project: CMakeLists.txt, CMakePresets.json for the preset configurations, a cmake/ directory, a documentation/ directory, examples/, include/, test/ and benchmark/. There is a .clang-format for the C++ formatting, a .clangd for the language server configuration, and a .cmake-format for the CMake files themselves.

The examples folder has a CMakeLists.txt and a mandelbrot subdirectory, which is a good sign for a concurrency library. A Mandelbrot renderer is a natural fit for a thread pool because it is embarrassingly parallel with uneven per-iteration cost, which is exactly the workload shape where a fixed pool without work stealing is expected to do worst.

## Where this sits against the standard library and a work stealing pool

The realistic alternative is not another third party library in most cases. It is deciding not to use a thread pool at all.

C++20 gave the standard library a std::jthread, and the execution policies std::execution::par and par_unseq. For a batch of independent tasks where the work is large enough to amortise thread creation, a parallel algorithm over a range is often the better answer, because it needs no pool, no queue and no shutdown protocol. The cost is that you need the work to be expressible as a range operation.

When you do want a pool, the other comparison is with the work stealing designs. The benchmark summary already makes the point: riften::ThreadPool pulls ahead in certain situations, likely because it uses a lock-free queue, with a custom semaphore and a different approach to work stealing. A work stealing pool suits a program whose tasks have unpredictable durations, because an idle worker can take work from a busy worker instead of waiting for the shared queue to hand it something. A fixed pool with a shared queue suits a program whose tasks are uniform, and it is simpler to reason about and to shut down.

Against another fixed-pool library the differences are small, as the table shows, and the deciding factor becomes integration rather than speed. Header only versus a compiled library, one header directory versus a package manager entry, a result handle that blocks the calling thread versus a callback, and how cleanly the pool shuts down. This library gives you all of that in a single directory, which is the reason to pick it.

## Conclusion

Adopt dp::thread-pool if you are on C++20 and want a thread pool you can vendor as a single header, with detached and future-returning enqueue calls and a batch wait. Do not adopt it if you need work stealing, because the benchmark notes say riften's pool pulls ahead precisely because it uses a lock-free queue, and task_thread_pool seems to pull ahead with large numbers of smaller tasks. Before you adopt it, note that the last tagged release is 0.7.0 from 2025-01-01 while the last push was on 2026-10-20, so the branch is active but the release tags are not, and check the benchmark results for your compiler rather than the summary.

## FAQ

### How do I add dp::thread-pool to a CMake project?

The library is header only with everything under include/thread_pool, and it defines the CMake target dp::thread-pool. Use `find_package(thread-pool REQUIRED)` and link against dp::thread-pool, or pull it in with CPM based on Fetch_Content, disabling the TP_BUILD_TESTS, TP_BUILD_BENCHMARKS and TP_BUILD_EXAMPLES options.

### What are the three ways to enqueue a task in dp::thread-pool?

enqueue_detach runs a task with no result tracked, enqueue returns a handle whose get call blocks until the task is complete, and wait_for_tasks waits for a batch of detached tasks to finish. The pool itself is created with a thread count, for example dp::thread_pool pool(4).

### How does dp::thread-pool compare to other thread pool libraries?

The README's benchmark summary says dp::thread_pool is faster in most cases, especially when std::move_only_function is available, but that riften::ThreadPool pulls ahead in certain situations, likely because it uses a lock-free queue, and that task_thread_pool pulls ahead with large numbers of smaller tasks. Full MSVC and Clang results are published in the benchmark results folder.

### Is dp::thread-pool available through vcpkg?

Yes. The port is named dp-thread-pool and installs with `vcpkg install dp-thread-pool`. Note that the GitHub account for the project was renamed from DeveloperPaul123 to ptsouchlos, with ownership and history unchanged.

## Sources

- [License: MIT](https://github.com/ptsouchlos/thread-pool/blob/main/LICENSE)
- [Project website](https://DeveloperPaul123.github.io/thread-pool)
- [ptsouchlos/thread-pool on GitHub](https://github.com/ptsouchlos/thread-pool)
- [README](https://github.com/ptsouchlos/thread-pool/blob/main/README.md)
- [Releases](https://github.com/ptsouchlos/thread-pool/releases)

---

Hysen Labs editorial analysis, written from the project's own repository and release notes. Cite the canonical page: https://hysenlabs.com/projects/ptsouchlos-thread-pool
