libfork: fork-join tasking on C++20 coroutines
A bleeding-edge, lock-free, wait-free, continuation-stealing tasking library built on C++20's coroutines
At a glance
- What is it?
- libfork is a header-only C++20 library that expresses fork-join parallelism with coroutines instead of threads or macros, and it decouples scheduling from task writing. It suits performance-minded C++ developers who want very low task overhead and a replaceable scheduler.
- Who is it for?
- libfork is a good fit for a C++20 codebase that spawns many short tasks, that already separates what a task does from how tasks are scheduled, and that is willing to depend on a library whose last tagged release is still v3.8.0 from 2024-05-10 while the last push was on 2026-05-25.
- Can I use it commercially?
- Yes, with conditions. MPL-2.0 is a weak copyleft licence: you can use it inside commercial and closed-source software, but if you distribute changes to its own files, you must publish those changes under the same licence.
- Is it still maintained?
- Yes. The repository last received commits 151 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 21, 2026, and from our analysis. They are not legal advice.
Editorial analysis
Fork-join written as coroutines, not as threads
The idea libfork is built on is that fork-join parallelism should be a language feature rather than a library convention. Most C++ code expresses fork-join with threads, futures, or a task graph handed to a scheduler, and all three make the programmer reason about lifetimes and synchronisation explicitly. libfork uses C++20 coroutines so that the fork, the call and the join become three awaitable expressions. The whole API is visible in a short Fibonacci example:
#include "libfork/core.hpp"
inline constexpr auto fib = [](auto fib, int n) -> lf::task<int> {
if (n < 2) {
co_return n;
}
int a, b;
co_await lf::fork[&a, fib](n - 1); // Spawn a child task.
co_await lf::call[&b, fib](n - 2); // Execute a child inline.
co_await lf::join; // Wait for children.
co_return a + b; // Safe to read after a join.
};The three verbs map to three different execution strategies. A fork spawns a child task, a call executes a child inline on the current thread, and a join waits for the children to finish. The captures are passed by reference, which is why the comment on the last line notes that reading a and b is safe only after the join. This is the main reason the library is not a drop-in for other approaches: the safety of a fork-join program depends on the discipline of where you place the join.
The cactus stack and why task spawn cost is the design centre
The performance claim in the project description is that the library is engineered for performance, and the mechanism it credits is an almost non-allocating cactus-stack built on segmented stacks, which is what enables ultra-fine-grained parallelism, meaning the ability to spawn tasks with very low overhead. The alternative to a cactus stack is a fixed size thread stack per task, and that is the actual cost that dominates when your tasks are small. A cactus stack shares the frames of a task's children with the parent rather than giving each task an independent stack, so a deep tree of tiny tasks does not multiply stack memory. The segmented stacks are described as almost non-allocating, which is doing some quiet work in that sentence: not zero allocation, but close enough that the allocator is not the bottleneck. This is the layer that makes the rest of the design sensible, because a tasking system is only worth using if the cost of creating a task is small compared to the work the task does, and the segmented stack is the piece that keeps that ratio in your favour.
Scheduling is a customization point, task writing is not
The architectural commitment worth noting is that libfork presents a cross-platform API that decouples scheduling tasks from writing tasks, with scheduling described as a customization point. In practice that means the fork, call and join expressions in your task code do not bind you to a particular runtime, and the project also provides performant NUMA-aware work-stealing schedulers for general use. The distinction is doing real work in the design, because most tasking libraries make the scheduler part of the API and force you to write tasks that mention it. Here a task is written as a coroutine returning lf::task and it stays that way. The cost of that separation is that you have to learn two things rather than one: the task vocabulary, and then the scheduler you plug in. For a library aimed at systems programmers that is a reasonable trade, since the scheduler is the part you are most likely to want to replace or to benchmark against, and the authors have made it swappable rather than baked in.
The performance numbers come from a paper, not from the README
The headline results are attributed to a paper, and it is worth being clear about what kind of evidence that is. The README points to a paper on arXiv for a detailed review of libfork on 1 to 112 cores, and describes the headline result as linear time and memory scaling. From that, the listed comparisons are up to 7.5 times faster and 19 times less memory than OneTBB, up to 24 times faster and 24 times less memory than openMP through libomp, and up to 100 times faster with more than 100 times less memory than taskflow. The word up to matters in each of those, since a worst case claim is the friendly way to express a range. The repository also carries a comprehensive benchmark suite under bench, and the README uses the average time to spawn and run a task during the recursive Fibonacci benchmark as an approximation of tasking overhead and peak throughput, and quotes peak physical memory allocation during the T3L unbalanced tree search benchmark for the memory comparison. Note that the numbers are attributed to a document rather than being presented as results you can reproduce by running one command, so treat them as a reason to look at the paper and the benchmark suite rather than as a specification.
Consuming a header-only library without any dependencies
libfork is header-only with full CMake support and zero required dependencies, and the documentation recommends consuming it through a package manager because that simplifies the management of optional dependencies. The vcpkg route adds one entry to vcpkg.json:
"dependencies": [
"libfork"
]The Conan route is a single command, with the note that you need a C++20 ready conan profile:
conan install --requires="libfork/[*]" --build=missingEither way the library is then consumed in CMake through the same two calls, find the package and link the namespaced target:
find_package(libfork CONFIG REQUIRED)
target_link_libraries(
project_target PRIVATE libfork::libfork
)Both package managers carry a note that the port and recipe are maintained by their own teams and community contributors, and that an out of date version should be reported upstream on the vcpkg or conan-center-index repository rather than patched locally. That is a practical point: the version you get from a package manager can lag the repository, and the two have different maintainers. If you install from source, BUILDING.md covers compiling the tests, benchmarks and docs, installation, optional dependencies and the developer tooling.
FetchContent, submodules and the single header escape hatch
Three other integration routes exist, and they suit different constraints. FetchContent downloads and builds libfork as part of your project without installing it, and pins a tag:
include(FetchContent)
FetchContent_Declare(
libfork
GIT_REPOSITORY https://github.com/conorwilliams/libfork.git
GIT_TAG v3.7.2
GIT_SHALLOW TRUE
)
FetchContent_MakeAvailable(libfork)The git submodule route is the same idea with a different tool, where a clone into external/libfork is picked up by add_subdirectory. And there is a single header, libfork.hpp, that you can copy and paste into a project. That last option is explicitly not recommended and exists mainly for easy integration with godbolt, the online compiler explorer, where you want a task in the editor without setting up a build. It is worth knowing it exists, because an evaluator will find it, but the note about hwloc integration and compiler flags in BUILDING.md is the reason to treat it as a demo convenience rather than a distribution method. The full API reference lives in generated documentation published on the project's documentation site.
Releases stopped in 2024 while the branch kept moving
The maintenance picture is mixed, and it is the main thing to weigh before depending on this. The last push was on 2026-05-25, so the source is being worked on, but the most recent tagged releases are v3.8.0 from 2024-05-10, v3.7.2 from 2024-03-18 and v3.7.1 from 2024-02-29. The gap between a recent commit and a release more than two years old is the shape of a project where the main branch is active but releases lag, and it has a direct consequence for the FetchContent example above, which pins v3.7.2 rather than the newest commit. If you take the package manager route, the version you receive depends on when the port was last refreshed, and the two notes about the vcpkg port and the Conan recipe being maintained elsewhere mean you may be several versions behind. The licence field on the repository says MPL-2.0, which is a file-level copyleft licence, so the practical question for a commercial project is how it interacts with your own code, and the repository also carries a CITATION.cff, a ChangeLog.md and a TODO.md.
Editorial conclusion
libfork is a good fit for a C++20 codebase that spawns many short tasks, that already separates what a task does from how tasks are scheduled, and that is willing to depend on a library whose last tagged release is still v3.8.0 from 2024-05-10 while the last push was on 2026-05-25. It is the wrong tool for a codebase that is not yet on C++20, for a project that needs a stable release cadence, and for anyone who wants a general purpose thread pool, since the schedulers here are built for a fork-join task graph rather than for general work queues. Before adopting it, read BUILDING.md for the compiler flags and the optional hwloc integration, confirm your toolchain supports the C++20 coroutine features the library relies on, and check the single header path only if you intend to paste it into a project, since the documentation says that route is not recommended.
Frequently asked questions
What is libfork and what problem does it solve?
libfork is a header-only C++20 library for fully portable, strict fork-join parallelism, expressed with coroutines rather than macros or inline assembly. It separates scheduling tasks, which is a customization point, from writing tasks, and it ships NUMA-aware work-stealing schedulers.
How do I add libfork to a project with vcpkg?
Add libfork to the dependencies array in your vcpkg.json, then consume it in CMake with find_package(libfork CONFIG REQUIRED) and link the libfork::libfork target. The vcpkg port is maintained by Microsoft team members and community contributors, so an out of date version should be reported on the vcpkg repository.
How do I install libfork with Conan?
Run conan install --requires="libfork/[*]" --build=missing, or add libfork/[*] to your conanfile recipe requirements, and make sure you use a C++20 ready conan profile. The library is then found with find_package and linked through the libfork::libfork target.
What are the differences between lf::fork, lf::call and lf::join?
In the documented Fibonacci example, lf::fork spawns a child task, lf::call executes a child inline, and lf::join waits for the children so that the captured results are safe to read. The captures are passed by reference, which is why the join has to happen before the values are used.
Is libfork still being released?
The last push was on 2026-05-25, but the most recent tagged releases are v3.8.0 from 2024-05-10, v3.7.2 from 2024-03-18 and v3.7.1 from 2024-02-29. The licence field on the repository says MPL-2.0, and the repository carries a ChangeLog.md, a TODO.md and a CITATION.cff.
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/conorwilliams-libfork)Was this useful?