task_graph 的核心只有几个概念,但组合起来足以覆盖从图像流水线到推理管路的大部分场景。本文按数据流动的顺序过一遍。

Task 与 DAG

一切从 Task 开始:它有一个 ID、一个执行体(lambda 或子类化的 INode),以及最重要的 —— 端口契约

auto process = std::make_shared<Task>("process", [](TaskContext& ctx) {
    auto data = ctx.input<std::string>("in");   // 从上游端口读数据
    return TaskResult{.status = TaskStatus::COMPLETED,
                      .value = std::string(*data + "_processed")};
});

DAG 是任务的容器:add_task 注册,connect("fetch", "process") 建边(默认接 out → in 端口,也支持端口限定的多端口边)。

端口:类型化的数据流

  • 任务间通过命名端口交换数据,载荷是 std::any
  • 读上游输出用 ctx.input<T>("port")
  • 返回结果放 TaskResult.value(默认输出端口 "out"),或通过 outputs 一次写多个端口;
  • 跨动态库使用的自定义类型必须注册:TG_REGISTER_TYPE(MyType, "my::Type") —— 稳定的字符串名保证跨 SO 边界的类型一致性。

执行器:拓扑并行

DAGExecutor 内置线程池,按依赖关系自动调度:无依赖关系的分支并行跑,有依赖的串行等待。执行完成后每个任务都能查到状态与耗时:

DAGExecutor executor;
executor.execute(dag).wait();
for (auto& [id, r] : executor.get_results())
    std::cout << id << ": " << (r.is_success() ? "SUCCESS" : "FAILED") << "\n";

任务失败不会让整图崩溃:下游被跳过,失败原因随 TaskResult 带回(WASM/移动端以 -fno-exceptions 构建,插件边界一律不抛异常、只返回状态)。

插件模型:编译期 + 运行时

两种扩展方式,共享同一套 INode 接口(type() / execute() / input_specs() / output_specs() / param_specs()):

方式机制适合
子模块(subnode)编译期链接,subnode.json + cmake/Subnode.cmake 接入官方插件(OpenCV、GPU、JS、MediaPipe)
动态插件运行时 PluginLoader dlopen,导出 register_plugin,SDK 版本不匹配会拒绝加载第三方分发、按需装载

新插件不用手写脚手架:python scripts/generate_submodule.py 一键生成 CMakeLists、任务类与双重注册代码。

JSON 序列化(v2.0)

图是带版本号的纯数据(见快速上手中的示例):任务数组 + 边数组,边可带 from_port / to_portDAGSerializer::from_string 一行加载,GraphStudio 编辑器读写的也是同一格式。

GPU 后端:全部 opt-in

Metal(Apple)/ Vulkan / CUDA 三个后端由 CMake 开关控制(TASK_GRAPH_ENABLE_METAL/VULKAN/CUDA,默认全关):需要 GPU 的任务在无后端环境优雅降级,测试在 CI 上自动 soft-skip。GPU 子模块提供 gpu_box_blurgpu_gaussian_blurgpu_resize 等图像算子。

跨平台形态

  • 桌面libtask_graph 为 SHARED 库,支持 dlopen 动态插件;
  • iOS / Android / WASM:STATIC 库 + -fno-exceptions,同一套图定义与任务代码复用。

小结

「typed port + 拓扑并行 + JSON 图 + 双层插件」是 task_graph 的全部骨架。想动手跑一遍?从快速上手 GraphStudio开始;想出包分发?看从源码构建三端安装包