
build2 源码剖析DAG 构建模型与调度器并行构建原理【免费下载链接】build2build2 build system项目地址: https://gitcode.com/gh_mirrors/bu/build2build2 是一款现代化的开源 C/C 构建系统其最吸引人的特性之一就是基于DAG 构建模型有向无环图与调度器并行构建的设计。本文将从源码层面剖析 build2 如何用一张依赖图描述整个工程并通过自研调度器让多个编译任务真正并行执行帮助读者理解并行构建原理也为想深入阅读 build2 源码的开发者提供一份地图。为什么说 build2 的并行构建不简单很多构建系统如早期的 make是串行执行的编译完 A 再编译 B。而 build2 在设计的起点就把并行调度写进了核心libbuild2/scheduler.hxx中明确写道调度器最适合执行重量级任务例如运行一个编译进程因为在这种情况下线程同步的开销相对于任务本身可以忽略不计。这意味着 build2 的并行不是简单开几个线程而是一套完整的任务调度体系。DAG 构建模型一切从依赖图开始在 build2 的世界里整个构建过程可以被抽象成一张有向无环图DAG节点target每一个需要被构建的对象比如hello.o、hello可执行文件边prerequisite目标之间的依赖关系比如hello.o依赖hello.cxxhello依赖hello.o。因为这张图是无环的所以 build2 才能安全地并发调度只有当一个节点的所有前置依赖都构建完成后它才能开始构建。这张图的数据结构定义在libbuild2/target.hxx而图的构建与遍历逻辑集中在libbuild2/algorithm.cxx中。目标状态机并行安全的生命周期为了让多个线程同时操作一张依赖图而不出错build2 为每个目标定义了一套状态机见libbuild2/target-state.hxx状态含义unknown尚未处理unchanged已检查无需重新构建增量构建的成果postponed被推迟如最后一个执行模式下的中间目标busy正在被某个线程构建中changed构建完成且内容发生了变化failed构建失败group状态继承自其所属的目标组这套状态机是并行构建的基石多个线程看到同一个目标时通过原子操作竞争busy状态从而保证同一个目标永远不会被两个线程同时构建。匹配与执行构建的两个关键阶段在libbuild2/algorithm.cxx中build2 把构建拆成两个阶段匹配阶段match为目标找到合适的规则rule并确定它是否需要重新构建。对应源码中的match_impl()函数它通过lock_impl()获取目标锁然后依次经历tried → touched → matched → applied等内部偏移状态执行阶段execute真正运行编译命令或配方recipe对应execute_impl()函数。有趣的是这两个阶段本身也是可以并行的——匹配可以按依赖顺序并发进行执行同样如此。这为调度器提供了大量可并发的任务。调度器master 线程与 helper 线程的协作build2 的并行核心是libbuild2/scheduler.hxx中定义的scheduler类它的设计思路非常优雅master 线程负责发起并行任务。比如要更新一个目标的所有前置依赖时master 会通过async()把每个前置依赖的构建外包出去helper 线程调度器维护的一批工作线程专门执行 master 交给它们的任务。关键机制是调度器保证任意时刻处于活跃状态的线程数量不超过硬件线程数。当 master 调用wait()等待任务完成时它会被挂起suspended腾出的名额让其他线程得以激活任务完成后的 helper 线程也可能被挂起转而去唤醒一个等待中的 master。一个容易忽略的细节是挂起的线程不会被复用为 helper而是总是新建 helper 线程。源码注释解释得很清楚——这是为了让就绪的 master能尽快继续运行避免它被嵌套的wait()阻塞在调用栈深处。所以调度器实际创建的线程数通常会超过最大活跃线程数。async 与 wait并行构建的原语并行调度的两个核心原语定义在scheduler.hxx中async(start_count, task_count, ...)把任务提交到调度器。如果 helper 线程可用任务被异步执行并返回true否则任务会在wait()中同步执行甚至直接在async()调用内同步执行wait(start_count, task_count, work_queue)等待任务计数降到起始值以下。这里task_count是一个std::atomicsize_t原子计数它是线程间同步的关键。wait()还有一个work_queue参数控制等待期间是否顺便处理自己队列里的任务work_none完全不处理、work_one每做完一个任务就检查一次、work_all先清空队列再检查。这种等待时不闲着的设计让线程在阻塞期间也能贡献算力是提升并行效率的重要技巧。在algorithm.cxx的执行逻辑中我们可以清楚地看到这种用法目标从applied状态原子地切换到busy状态通过compare_exchange_strong实现然后调用ctx.sched-async(...)把execute_impl()交给调度器去并行执行最后通过fetch_sub递减任务计数并调用sched-resume()唤醒等待者。依赖计数与最后一个执行模式libbuild2/context.hxx中维护了两个重要的全局计数dependency_count尚未满足的依赖关系数量target_count尚未执行的目标数量。它们配合execution_mode执行模式一起工作。比如last模式下一个目标只有当它是最后一个被依赖者时才真正执行——这在清理clean操作中非常有用多个成员依赖同一个组时组只需被清理一次。execute_impl()中--td ! 0时直接返回postponed状态的逻辑正是这种模式的实现。死锁检测并行构建的安全网并行调度最大的风险是死锁。build2 的调度器内置了一套基于进度监测的死锁检测机制如果活跃线程长时间没有取得进展即没有完成任务调度器就会触发诊断。源码中大量出现的deactivate()/activate()调用是线程在等待外部事件如文件系统操作时主动让位给其他线程的手段同时也会暂停死锁检测的计时——因为等待外部事件的时间不算卡死。从哪几行代码开始读如果你想深入这套 DAG 构建模型与并行构建原理建议按下面的顺序阅读源码libbuild2/target-state.hxx目标状态机的完整定义是理解一切的基础libbuild2/scheduler.hxx与libbuild2/scheduler.cxx调度器的接口与实现重点看async()、wait()、deactivate()/activate()libbuild2/algorithm.cxxmatch_impl()与execute_impl()看依赖图如何被并发遍历libbuild2/context.hxx执行模式与全局计数的定义。总结build2 的并行构建原理可以浓缩为一句话用状态机保证依赖图的并发安全用任务调度器实现线程的高效协作。DAG 构建模型让 build2 能精确地知道谁先谁后而调度器让这些可以并行的任务真正跑满 CPU。理解了这两层设计你也就理解了现代高性能构建系统的核心思想——这不仅是阅读 build2 源码的钥匙对设计任何并发系统都有启发意义。【免费下载链接】build2build2 build system项目地址: https://gitcode.com/gh_mirrors/bu/build2创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考