一、简介
cpp-taskflow 源码:https://github.com/cpp-taskflow/cpp-taskflow
(后面简称taskflow)
taskflow一个写的比较好的基于task有向无环图(DAG)的并行调度的框架,之所以说写的比较好,个人觉得有几点原因:
1.是一个兼具学术研究和工业使用的项目,并非一个玩具
2.现代C++开发,风格简洁
(源码要求编译器支持C++17,也比较容易改成C++11)
3.文档全面
4.注释丰富
因此,学习和研究了taskflow的代码,并写此文作为学习笔记。
通读代码之后,个人的感觉是开发一个通用的DAG调度框架重要在三个方面:
1.拓扑结构的存储和表达
taskflow存储和表达拓扑结构的方式还是比较简单的,用Node类表示DAG中的每个结点,Graph类存储所有的Node对象,Topology类表示一个拓扑结构,后面再详细说。
2.拓扑结构的调度执行
taskflow的调度逻辑中为提高性能做了较多优化:通过WorkStealingQueue提高线程使用率,通过Notifier类(from Eigen库)减少生产者-消费者模式中加锁的频率等。
3.辅助工具或功能
taskflow提供方法可以比较简单的监视每个线程的活动、分析用户程序的性能。
二、基本数据类型
1.类Node
类Node最重要的是保存了当前节点的执行函数,所有前继节点的指针、后续节点的指针,子图(如果有的话),每个前继节点完成后需要修改的计数器(当所有的前继节点都完成时,当前节点就可以执行了)。
class Node {
//static节点的执行函数是返回值和参数都是void的函数
using StaticWork = std::function<void()>;
//dynamic节点的执行函数是返回值为void、参数为Subflow的函数,
//在代码中通过模板元编程判断参数是否是Subflow来区分静态节点还是动态节点
using DynamicWork = std::function<void(Subflow&)>;
//节点的status
constexpr static int SPAWNED = 0x1;
constexpr static int SUBTASK = 0x2;
public:
Node() = default;
//可以通过构造函数传入节点的执行函数,可以不传入(default构造函数),后续再set
template <typename C>
Node(C&&);
//传入当前节点的一个后续节点
void precede(Node&);
private:
std::string _name;
//用了C++17的std::variant函数,可以认为就是union类型,未设置、static work、dynamic work
std::variant<std::monostate, StaticWork, DynamicWork> _work;
//所有的后续节点
tf::PassiveVector<Node*> _successors;
//所有的前继节点
tf::PassiveVector<Node*> _dependents;
//子图,正如github上的文档所说,每个节点可以在执行期动态的创建子图
//这里用了C++17的std::optional函数,多了”未设置“状态,好处是如果未设置不会调用Graph的构造函数
std::optional<Graph> _subgraph;
//若有值,表示此节点实际一个Taskflow,通过组合成为了一个Node
Taskflow* _module {nullptr};
//以下两个是弱引用
Topology* _topology {nullptr};
Taskflow* _module {nullptr};
int _status {0};
//前继节点的个数,在调度执行过程会多线程修改,所以是std::atomic类型。
std::atomic<int> _num_dependents {0};
};
重点说下precede函数的实现
inline void Node::precede(Node& v) {
//1.将v添加到当前节点的后续节点vector中
_successors.pus