Libfork:C++20 协程驱动的革命性并行任务库

内容分享6天前发布
2 3 0

介绍

作为一名资深 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: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 和工作窃取调度器。

整体架构分为三层:

  1. 任务层(Task Layer):用户编写异步函数,返回 lf::task<T>。异步函数是一个 lambda,第一个参数是自引用(y-combinator),允许递归。内部使用 co_await 操作 fork/call/join。
  2. fork:并发 spawn 子任务,绑定返回地址。
  3. call:串行执行子任务,无需窃取。
  4. join:等待所有子任务完成,确保严格 fork-join。
  5. co_new:在 cactus-stack 上分配空间,支持 RAII。
  6. eventually:延迟构造返回类型,支持异常捕获。
  7. just:立即调用异步函数,无 fork-join 作用域。
  8. 栈管理层(Stack Management Layer):创新的 cactus-stack 使用 segmented stacks,每个协程分配在栈片段上,几乎无堆分配。每个 worker 有本地栈,fork 时延续可被窃取,但栈片段共享,避免拷贝。
  9. 调度层(Scheduling Layer):符合 lf::scheduler 概念的类型。内置:
  10. lazy_pool:NUMA 感知,工作窃取,线程睡眠等待工作。
  11. busy_pool:类似,但忙等待,适合空闲机器。
  12. unit_pool:单线程,用于测试。
  13. 调度通过 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 适用于需要高效并行的场景,尤其在多核系统中。

  1. 科学计算:如数值模拟、矩阵运算。示例:并行矩阵乘法。
 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 执行,适用于大规模矩阵。

  1. 游戏引擎:细粒度任务如物理模拟、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。

  1. 数据处理:如并行排序、搜索。示例:不平衡树搜索(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);
 };

内存高效,适合大数据集。

  1. 调试与测试:使用 unit_pool 单线程执行,验证逻辑。
 lf::unit_pool pool;
 lf::sync_wait(pool, fib, 10);
  1. 异常密集应用:如网络服务,使用 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;
 };
  1. 显式调度:在多池环境中,使用 resume_on。
 auto awaitable = lf::resume_on(&other_pool);
 co_await awaitable;

这些场景展示 Libfork 的 versatility,在高性能需求下优于传统库。

总结

Libfork 是 C++20 时代并发编程的杰作,其无锁、延续窃取设计和 cactus-stack 创新,使其在性能上领先竞品。作为资深开发者,我推荐它用于高并行应用:从快速上手到复杂场景,都能提供高效解决方案。未来,随着 C++ 进化,Libfork 将继续引领潮流。探索它,你会发现并发编程从未如此优雅。

© 版权声明

相关文章

3 条评论

  • 头像
    亿客行 读者

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

    无记录
    回复
  • 头像
    云深不知处 读者

    哥,有时间使用经验不?感觉如何?

    无记录
    回复
  • 头像
    三皮 读者

    收藏了,感谢分享

    无记录
    回复