介绍
作为一名资深 C++ 开发者,我一直关注现代 C++ 在并发编程领域的创新。Libfork 是一个前沿的、基于 C++20 协程的任务库,它专注于提供无锁(lock-free)、无等待(wait-free)的延续窃取(continuation-stealing)任务调度机制。这个库由 Conor Williams 开发,托管在 GitHub 上(
https://github.com/ConorWilliams/libfork),其核心目标是为严格的 fork-join 并行模型提供高效、可移植的抽象,而无需使用宏或内联汇编。

Libfork:C++20 协程驱动的革命性并行任务库
Libfork 的诞生源于对传统任务库(如 OneTBB、OpenMP 和Taskflow)在性能和内存消耗上的不满。它利用 C++20 协程实现了极细粒度的并行任务创建,任务开销极低(约为普通函数调用的 10 倍)。库的设计强调跨平台 API,将任务调度与任务编写解耦,用户可以自定义调度器,同时提供内置的 NUMA 感知工作窃取调度器。
在实际开发中,Libfork 特别适合需要高性能并行的场景,如科学计算、游戏引擎或数据处理。它支持零依赖的头文件式集成,通过 CMake 轻松集成到项目中。库的名称“libfork”巧妙地呼应了 fork-join 模型,同时暗示了其“叉子”(fork)的核心操作。目前,Libfork 已更新到 v3.7.2 版本,并通过 vcpkg 和 Conan 等包管理器分发。
Libfork 的亮点在于其创新的仙人掌栈(cactus-stack)实现,这是一种几乎不分配内存的分段栈(segmented stacks),使得协程分配类似于线性栈分配,从而显著降低内存使用和任务创建开销。根据基准测试,Libfork 在 1-112 核上的表现优异:线性时间/内存缩放,比 OneTBB 快 7.5 倍、内存少 19 倍;比 OpenMP 快 24 倍、内存少 24 倍;比 Taskflow 快 100 倍、内存少 100 倍以上。
总之,Libfork 不是一个简单的任务库,而是 C++ 并发编程的革命性工具,它将协程的潜力发挥到极致,让开发者能以简洁的语法编写高效的并行代码。
特性
Libfork 的特性主要围绕性能、可移植性和易用性展开。第一,它是完全头文件式的库,无需外部依赖(可选依赖如 hwloc 用于 NUMA 优化),支持 CMake 集成,便于在各种项目中使用。
关键特性包括:
- 无锁、无等待的延续窃取:采用 continuation-stealing 机制,当一个任务 fork 出子任务时,当前线程立即执行子任务,而延续(continuation)可被其他线程窃取。这避免了传统工作窃取的开销,提高了吞吐量。
- 严格 fork-join 并行:库强制所有子任务在父任务返回前必须 join,这确保了任务 DAG(有向无环图)的数学属性,便于优化。语法类似于 Cilk,支持 fork(并发执行)、call(串行执行)和 join。
- 仙人掌栈实现:使用分段栈的 cactus-stack,几乎消除协程的堆分配开销,支持极细粒度任务(ultra-fine grained parallelism)。这使得任务创建速度极快,内存消耗低。
- NUMA 感知调度器:内置 lazy_pool 和 busy_pool 支持 NUMA(非统一内存访问)架构,优化多核系统的内存访问。unit_pool 用于单线程调试。
- 异常支持:协程内异常会被捕获并在 join 时重新抛出,支持 try-catch 和 stash_exception 机制,避免 UB(未定义行为)。
- 延迟构造和引用限制:通过 lf::eventually 处理非默认构造类型,支持引用但限制 r-value 引用以防悬垂。
- 上下文切换 API:允许自定义 awaitable,实现显式调度,如 resume_on。
- 基准测试套件:全面的性能测试,覆盖 Fibonacci、树搜索等场景,证明其优于竞品。
此外,Libfork 的 API 设计简洁,使用 lambda 定义异步函数,支持递归。通过 co_await 操作 fork/call/join,代码可读性高。库还提供单头文件版本,便于快速实验(如在 Compiler Explorer 上)。
在模块分类上,Libfork 分为核心(core)和调度(schedule)两部分:
- 核心模块(libfork/core.hpp):包含 task<T>、fork、call、join、co_new、eventually 等,用于编写任务。
- 调度模块(libfork/schedule.hpp):包含 lazy_pool、busy_pool、unit_pool 和 scheduler 概念,用于执行任务。
- 扩展模块:用于自定义调度器,涉及 submit_handle 和 context_switcher。
这些特性使 Libfork 在高性能计算中脱颖而出,尤其适合对开销敏感的应用。
架构
Libfork 的架构设计精巧,核心是基于 C++20 协程的 fork-join 模型,结合 cactus-stack 和工作窃取调度器。
整体架构分为三层:
- 任务层(Task Layer):用户编写异步函数,返回 lf::task<T>。异步函数是一个 lambda,第一个参数是自引用(y-combinator),允许递归。内部使用 co_await 操作 fork/call/join。
- fork:并发 spawn 子任务,绑定返回地址。
- call:串行执行子任务,无需窃取。
- join:等待所有子任务完成,确保严格 fork-join。
- co_new:在 cactus-stack 上分配空间,支持 RAII。
- eventually:延迟构造返回类型,支持异常捕获。
- just:立即调用异步函数,无 fork-join 作用域。
- 栈管理层(Stack Management Layer):创新的 cactus-stack 使用 segmented stacks,每个协程分配在栈片段上,几乎无堆分配。每个 worker 有本地栈,fork 时延续可被窃取,但栈片段共享,避免拷贝。
- 调度层(Scheduling Layer):符合 lf::scheduler 概念的类型。内置:
- lazy_pool:NUMA 感知,工作窃取,线程睡眠等待工作。
- busy_pool:类似,但忙等待,适合空闲机器。
- unit_pool:单线程,用于测试。
- 调度通过 sync_wait 启动根任务。自定义调度器需实现 submit 和 reschedule。
架构确保跨平台(无需 asm),并支持扩展,如集成 hwloc 优化 NUMA。异常流在 join 时处理,避免 UB。上下文切换通过 submit_handle 实现显式控制。
这种分层设计使 Libfork 高效且灵活,用户只需关注任务编写,调度可自定义。
快速上手
要快速上手 Libfork,第一通过包管理器安装。
使用 vcpkg:
在 vcpkg.json 中添加:
"dependencies": [
"libfork"
]
然后在 CMakeLists.txt:
find_package(libfork CONFIG REQUIRED)
target_link_libraries(your_target PRIVATE libfork::libfork)
使用 Conan:
conan install --requires="libfork/[*]" --build=missing
CMake 同上。
FetchContent:
include(FetchContent)
FetchContent_Declare(
libfork
GIT_REPOSITORY https://github.com/ConorWilliams/libfork.git
GIT_TAG v3.7.2
GIT_SHALLOW TRUE
)
FetchContent_MakeAvailable(libfork)
target_link_libraries(your_target PRIVATE libfork::libfork)
目前,编写第一个程序:计算 Fibonacci。
#include <libfork/core.hpp>
#include <libfork/schedule.hpp>
#include <iostream>
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);
co_await lf::call[&b, fib](n - 2);
co_await lf::join;
co_return a + b;
};
int main() {
lf::lazy_pool pool(4); // 4 线程
int result = lf::sync_wait(pool, fib, 10);
std::cout << "Fib(10) = " << result << std::endl;
return 0;
}
编译运行:g++ main.cpp -std=c++20 -pthread
忽略结果示例:
co_await lf::fork[fib](n - 1); // 无返回绑定
使用 co_new 分配:
#include <span>
#include <numeric>
inline constexpr auto sum_inputs = [](auto, std::span<int> inputs) -> lf::task<int> {
auto [outputs] = co_await lf::co_new<int>(inputs.size());
for (std::size_t i = 0; i < inputs.size(); ++i) {
co_await lf::fork[&outputs[i], some_async_func](inputs[i]);
}
co_await lf::join;
co_return std::accumulate(outputs.begin(), outputs.end(), 0);
};
延迟构造:
struct Difficult { Difficult(int) {} };
inline constexpr auto make_diff = [](auto) -> lf::task<Difficult> {
co_return 42;
};
inline constexpr auto demo = [](auto) -> lf::task<> {
lf::eventually<Difficult> res;
co_await lf::fork[&res, make_diff]();
co_await lf::join;
// 使用 res.value()
};
异常处理:
inline constexpr auto excep_demo = [](auto) -> lf::task<> {
co_await lf::fork[throwing_func]();
try {
might_throw();
} catch (...) {
// Stash if needed
}
co_await lf::join; // 可能抛出
};
使用 just:
int res = co_await lf::just[some_func](args);
自定义调度:实现 lf::scheduler 概念。
这些示例展示 Libfork 的简洁性,快速上手后可扩展到复杂应用。
应用场景
Libfork 适用于需要高效并行的场景,尤其在多核系统中。
- 科学计算:如数值模拟、矩阵运算。示例:并行矩阵乘法。
inline constexpr auto mat_mul = [](auto mat_mul, Matrix A, Matrix B) -> lf::task<Matrix> {
if (small(A)) {
co_return serial_mul(A, B);
}
// 分割矩阵
auto [A11, A12, A21, A22] = split(A);
auto [B11, B12, B21, B22] = split(B);
Matrix C11, C12, C21, C22;
co_await lf::fork[&C11, mat_mul](A11, B11);
co_await lf::fork[&C12, mat_mul](A11, B12);
co_await lf::fork[&C21, mat_mul](A21, B11);
co_await lf::call[&C22, mat_mul](A21, B12);
co_await lf::join;
// 合并 C
co_return combine(C11, C12, C21, C22);
};
使用 lazy_pool 执行,适用于大规模矩阵。
- 游戏引擎:细粒度任务如物理模拟、AI 计算。示例:并行粒子模拟。
inline constexpr auto simulate_particles = [](auto sim, std::span<Particle> particles) -> lf::task<> {
if (particles.size() < threshold) {
co_return serial_sim(particles);
}
auto mid = particles.size() / 2;
auto left = particles.subspan(0, mid);
auto right = particles.subspan(mid);
co_await lf::fork[sim](left);
co_await lf::call[sim](right);
co_await lf::join;
};
在游戏循环中 sync_wait。
- 数据处理:如并行排序、搜索。示例:不平衡树搜索(T3L 基准)。
inline constexpr auto tree_search = [](auto search, Node* node) -> lf::task<Result> {
if (leaf(node)) {
co_return compute(node);
}
Result left, right;
co_await lf::fork[&left, search](node->left);
co_await lf::call[&right, search](node->right);
co_await lf::join;
co_return merge(left, right);
};
内存高效,适合大数据集。
- 调试与测试:使用 unit_pool 单线程执行,验证逻辑。
lf::unit_pool pool;
lf::sync_wait(pool, fib, 10);
- 异常密集应用:如网络服务,使用 stash_exception。
struct TryInt {
std::optional<int> val;
std::exception_ptr ex;
};
inline constexpr auto try_func = [](auto) -> lf::task<TryInt> {
TryInt ret;
try {
ret.val = compute();
} catch (...) {
ret.ex = std::current_exception();
}
co_return ret;
};
- 显式调度:在多池环境中,使用 resume_on。
auto awaitable = lf::resume_on(&other_pool);
co_await awaitable;
这些场景展示 Libfork 的 versatility,在高性能需求下优于传统库。
总结
Libfork 是 C++20 时代并发编程的杰作,其无锁、延续窃取设计和 cactus-stack 创新,使其在性能上领先竞品。作为资深开发者,我推荐它用于高并行应用:从快速上手到复杂场景,都能提供高效解决方案。未来,随着 C++ 进化,Libfork 将继续引领潮流。探索它,你会发现并发编程从未如此优雅。





c++20的协程太难用了,协程本就是为了解决阻塞代码的问题,但c++20提供了么配套了么?阻塞操作就这几类;1.文件操作2.网络操作3.锁竞争4.延时操作 sleep等这些操作要和协程配套在一起才是完整的,让普通开发者可以使用的技术,否则就是实验室里的产物,开发者根本用不了。为什么java的协程(虚拟线)一推出就很受欢迎,因为他把这些难点在jdk层面帮助开发者处理好了。并解决了异步处理逻辑拆散,控制困难,调试苦难的问题。虚拟线程对开发者来说写同步逻辑代码,调试也是同步的,但在运行效果上接近异步性能。c++你为了发挥开发者的全部能力,可以保留现在全裸状态,也可以帮助普通开发者封装好协程配套的这些操作。对于大多数开发者,要把这些封装的高效,稳定是有难度的。
哥,有时间使用经验不?感觉如何?
收藏了,感谢分享