taskflow 源码阅读笔记-1

之前写了一篇介绍Taskflow的短文:传送门

Taskflow做那种有前后依赖关系的任务管理还是不错的,而且他的源码里运用了大量C++17的写法,觉得还是非常值得学习的,因此决定看一下他的源码,这里顺便写了一篇代码学习笔记。

概述

代码链接:

https://github.com/taskflow/taskflow
本文是commitid: b91df2c365c20fa4cb43951192f6939fbe876abf 版本的源码学习记录,其他版本可能会有不同

简介

简介可以参考代码仓库的README
简单来说它是一个实现DAG(有向无环图)的多线程任务管理库, 下图截取自代码仓库的README:
image.png

使用方法

使用方法见README,这里需要指出的是,作者分享了他的compile explorer https://godbolt.org/z/j8hx3xnnx, 我们可以在上面修改代码来看效果
这个是个纯头文件的库,只需要引用其头文件就行了
#include <taskflow/taskflow.hpp> // Taskflow is header-only
但是这个头文件怎么来的呢?
其实这个和Eigen的使用方法一样,在源码库中,有一个taskflow文件夹,里面全部都是.hpp文件,在最外层有一个taskflow.hpp文件,它是总的入口文件,我们只要include这个文件就行。
具体来说,如果我们想在我们自己的项目中使用它,
一种方法是直接将taskflow文件夹中的所有文件拷贝到我们自己的代码仓库,
另一种更好的方式就是和Eigen一样,把他做成一个deb包安装在机器上,这样我们就可以在多个项目中使用它了
不管怎么安装,最后在我们需要使用它的地方#include <taskflow/taskflow.hpp> 就行了
下面摘抄自代码仓库中的README:

#include <taskflow/taskflow.hpp>  // Taskflow is header-only

int main(){
  
  tf::Executor executor;
  tf::Taskflow taskflow;

  auto [A, B, C, D] = taskflow.emplace(  // create four tasks
    [] () { std::cout << "TaskA\n"; },
    [] () { std::cout << "TaskB\n"; },
    [] () { std::cout << "TaskC\n"; },
    [] () { std::cout << "TaskD\n"; } 
  );                                  
                                      
  A.precede(B, C);  // A runs before B and C
  D.succeed(B, C);  // D runs after  B and C
                                      
  executor.run(taskflow).wait(); 

  return 0;
}

代码学习

核心代码量

主要功能代码约1.5万行,在taskflow文件夹下:
image.png

代码实现

从上面的使用示例来看,它给我们展示了几个接口:

  • tf::Executor executor;
  • tf::Taskflow taskflow;
  • auto [A, B, C, D] = taskflow.emplace(… …);
  • A.precede(B, C); // A runs before B and C
  • D.succeed(B, C); // D runs after B and C
  • executor.run(taskflow).wait();

下面我们就从这几个接口入手来看其代码实现

tf::Taskflow

首先来看命名空间tf
tf是本代码仓库的根命名空间,所有的代码都是在这个命名空间下的
然后我们来看tf::Taskflow
代码位于 taskflow/taskflow/core/taskflow.hpp

定义tf::Taskflow taskflow时,
首先会构造其成员变量
其中实际会执行构造的只有 Graph _graph;
我们看下_graph的构造过程:
Gragh类只有一个成员变量 std::vector<Node*> _nodes; 它是一个指针的数组,因此也没什么额外的构造过程
再来看Graph类的无参构造函数,发现它直接是用的default构造函数,因此它也没做什么事情。
然后会调用构造函数:
因为没有传入参数,所以调用的是无参构造函数:

// Constructor
inline Taskflow::Taskflow() : FlowBuilder{_graph} {
}

Taskflow的构造函数啥都没有干,我们再看它的基类FlowBuilder的构造函数
FlowBuilder构造时需要传入_graph变量,这个变量是Taskflow类的成员变量,后面再看它是怎么构造的

// Constructor
inline FlowBuilder::FlowBuilder(Graph& graph) :
  _graph {graph} {
}

FlowBuilder的构造函数也啥都没有干,只是把_graph赋值给它自己的成员变量,注意Graph类只有移动构造函数,也就是说,此时Taskflow类中的_graph已经报废了
因为FlowBuilder类中只有_graph这一个成员变量,因此在构造阶段它没啥别的事情了

taskflow.emplace(…)

Taskflow类里没有emplace(…)这个方法,它是Taskflow的基类FlowBuilder的成员函数
emplace(…)方法有5个实现版本, 其中一个的定义如下:

template <typename C, std::enable_if_t<is_static_task_v<C>, void>*>
Task FlowBuilder::emplace(C&& c) {
  return Task(_graph._emplace_back("", 0, nullptr, nullptr, 0,
    std::in_place_type_t<Node::Static>{}, std::forward<C>(c)
  ));
}

然后,作者在is_static_task_v的地方实现了不同版本的萃取方法:

  • is_static_task_v
  • is_dynamic_task_v
  • is_condition_task_v
  • is_multi_condition_task_v
  • sizeof…(C )>1

示例代码中传入的是多个lamda表达式, 会匹配到 sizeof…©>1 这个版本,代码如下:

template <typename... C, std::enable_if_t<(sizeof...(C)>1), void>*>
auto FlowBuilder::emplace(C&&... cs) {
  return std::make_tuple(emplace(std::forward<C>(cs))...);
}

然后再依次调用 is_static_task_v 的版本,代码如上所示:
做的事情是:

  • 用传入的lamda表达式构造了一个Node,
  • 然后把这个Node放到_graph中,
  • 最后用_graph._emplace_back返回的Node*来构造一个Task实例返回

A.precede(B, C)

从上面的分析可以知道, A/B/C都是Task类的实例,
先来看下Task类的构造:

inline Task::Task(Node* node) : _node {node} {
}

Task类中没有其他成员变量,只有一个Node* __node, 因此Task中只维护一个Node, Task类是Node类的观察者

precede() 是Task类的方法, 它的实现如下:

template <typename... Ts>
Task& Task::precede(Ts&&... tasks) {
  (_node->_precede(tasks._node), ...);
  //_precede(std::forward<Ts>(tasks)...);
  return *this;
}

实际上是执行的Node类的precede
再来看Node类precede函数

inline void Node::_precede(Node* v) {
  _successors.push_back(v);
  v->_dependents.push_back(this);
}

这里涉及到两个变量:

SmallVector<Node*> _successors;  // 下一个要执行的节点
SmallVector<Node*> _dependents;  // 上一个执行的节点

这样用这两个向量把依赖关系保存起来

D.succeed(B, C)

和上一个函数类似,只是依赖关系改了一下:

template <typename... Ts>
Task& Task::succeed(Ts&&... tasks) {
  (tasks._node->_precede(_node), ...);
  //_succeed(std::forward<Ts>(tasks)...);
  return *this;
}

tf::Executor

Executor类的构造过程如下:

// 声明
explicit Executor(size_t N = std::thread::hardware_concurrency());

// 定义
inline Executor::Executor(size_t N) :
  _MAX_STEALS {((N+1) << 1)},
  _threads    {N},
  _workers    {N},
  _notifier   {N} {

  if(N == 0) {
    TF_THROW("no cpu workers to execute taskflows");
  }

  _spawn(N);

  // instantite the default observer if requested
  if(has_env(TF_ENABLE_PROFILER)) {
    TFProfManager::get()._manage(make_observer<TFProfObserver>());
  }
}

我们用的示例是无参的,因此N默认为std::thread::hardware_concurrency(), 即当前系统支持的并发线程数的估计值
然后设置:

  • _MAX_STEALS 为 (N+1)*2
  • 实例化N个std::thread
  • 实例化N个worker
  • 实例化参数为N的notifier

然后调用_spawn()函数启动任务

executor.run(taskflow).wait()

inline tf::Future<void> Executor::run(Taskflow& f) {
  return run_n(f, 1, [](){});
}

template <typename C>
tf::Future<void> Executor::run_n(Taskflow& f, size_t repeat, C&& c) {
  return run_until(
    f, [repeat]() mutable { return repeat-- == 0; }, std::forward<C>(c)
  );
}

template <typename P, typename C>
tf::Future<void> Executor::run_until(Taskflow&& f, P&& pred, C&& c) {

  std::list<Taskflow>::iterator itr;

  {
    std::scoped_lock<std::mutex> lock(_taskflows_mutex);
    itr = _taskflows.emplace(_taskflows.end(), std::move(f));
    itr->_satellite = itr;
  }

  return run_until(*itr, std::forward<P>(pred), std::forward<C>(c));
}

TODO: 这里还有很多没看的,先写到这里,有空继续补充。。。

用到C++功能

这个代码库使用了大量的modern C++的特性,下面列举一些:

  • std::forward
  • std::future
  • std::function
  • lamda表达式
  • 模版元编程
  • std::atomic
  • std::decay_t
  • std::is_void_v
  • std::monostate
  • std::tuple
  • std::make_tuple
  • std::get
  • std::array
  • std::index_sequence
  • std::memory_order_relaxed
  • std::is_invocable_v
  • std::add_lvalue_reference_t
  • std::find_if
  • std::find_if_not
  • std::distance
  • std::next
  • std::min_element
  • std::advance
  • std::lock_guard
  • std::mutex
  • std::invoke
  • std::enable_if_t
  • std::memcmp

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/352203.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

【新书推荐】2.6节 原码、反码和补码

回顾上一节中&#xff0c;我们讲解了整数的编码规则。 无符号整数编码规则&#xff1a;无符号整数全部都是正数&#xff0c;是什么就存什么。 有符号整数编码规则&#xff1a;有符号整数最高有效位为0是正数&#xff0c;最高有效位为1是负数。 本节内容&#xff1a;原码、反…

【C++】类和对象(中篇)(全网最细!!!)

文章目录 &#x1f354;一、类的六个默认成员函数&#x1f354;二、构造函数&#x1f35f;1、概念&#x1f35f;2、特性&#x1f369;默认构造函数 &#x1f354;三、析构函数&#x1f35f;1、概念&#x1f35f;2、特性&#x1f369;默认析构函数 &#x1f354;四、拷贝构造函数…

单片机开发板-硬件设计

开发板设计 1> 概述2> 功能2.1> GPIO类2.2> 通信类2.3> 显示类 3> 测试 1> 概述 开发板的定位&#xff1a;学会单片机&#xff1b; 目的越单纯&#xff0c;做的东西越好玩&#xff1b; 51开发板&#xff1a;DAYi STM32F103开发板&#xff1a;DAEr STM32F…

项目中从需求分析到研发上线

一、背景 应用系统从设想到需求到研发到上线会经历一些列工程化过程。比如经典的瀑布模型工作流&#xff0c;其实就是一个经过很多经验总结下来的工程方法。本节阐述项目中从需求到研发上线的过程。但是也有些根据不同的行业&#xff0c;不同的公司&#xff0c;不同管理者的风…

Go 知识for-range

Go 知识for-range 1. for-range 的用法1.1 数组1.2 切片1.3 字符串1.4 map1.5 chan 2. 原理2.1 数组2.2 切片2.3 字符串2.4 map2.5 chan 3. 总结 https://a18792721831.github.io/ 1. for-range 的用法 for-range 表达式用于遍历集合元素&#xff0c;比传统的for更加简单直观…

【微信小程序】15分钟倒计时(附带天数和时钟的实现方法在文章中)

这是制作的订单支付前倒计时&#xff0c;如果客户在规定时间内没能 支付&#xff0c;则系统自动删除&#xff0c;这样就以便有些商品冗余&#xff0c;当然了&#xff0c;这里只有分钟和秒钟&#xff0c;天数和时钟我写在了最底下&#xff0c;最后代码的显示第七行&#xff0c;可…

C++:引用

目录 概念&#xff1a; 引用的使用格式&#xff1a; 引用特性&#xff1a; 常引用 使用场景&#xff1a; 1、做参数 二级指针时的取别名 一级指针取别名 一般函数取别名 2、做返回值 函数返回值的原理&#xff1a; 引用的返回值使用&#xff1a; 引用和指针的对比&…

搭建AI问答和AI绘画小程序都需要做什么?

1、注册和认证小程序 在微信公众平台 注册&#xff0c;选择小程序类别即可。根据提示提交企业相关资质文件即可&#xff0c;注册后进行认证小程序&#xff0c;官方会收取300元认证费用。也可以私信我可以免掉300元认证费。 2、开通微信商家支付 认证通过后&#xff0c;在“功…

uniapp 使用echarts做折线图条形图。

提前10天把中烟活动做完了&#xff0c;以为能打酱油到除夕那天&#xff0c;结果又要做什么数据看板&#xff0c;方便烟草领导过年查看数据&#xff0c;还只给5天时间&#xff0c;真实压榨剥削啊&#xff0c;下辈子再也不‘拍黄片’了&#xff0c;不&#xff01;下份工作我就转前…

MySQL:函数

基本介绍 在MySQL中&#xff0c;为了提高代码重用性和隐藏实现细节&#xff0c;MySQL提供了很多函数。函数可以理解为别人封装好的模板代码。 在MySQL中&#xff0c;函数非常多&#xff0c;主要可以分为五类&#xff1a;聚合函数、数学函数、字符串函数、日期函数、控制流函数、…

Maven讲解

介绍 Maven是一个流行的构建工具和项目管理工具&#xff0c;它主要用于Java项目的构建、依赖管理和项目报告生成。Maven通过提供一致的项目结构、自动化的构建过程和强大的依赖管理&#xff0c;简化了项目的开发和维护过程。 下面是一些Maven的主要特点和用途&#xff1a; 项…

【算法】Knuth-Morris-Pratt 算法(KMP算法):一种在字符串中查找子串的算法

引言 KMP&#xff08;Knuth-Morris-Pratt&#xff09;算法是一个在字符串中查找子串的算法&#xff0c;由 Donald Knuth、Vaughan Pratt 和 James H. Morris 共同发明。这个算法的特点是在查找过程中&#xff0c;不会回溯主串&#xff0c;也不会重复扫描已经比较过的子串&…

2024年上海高考数学最后四个多月的备考攻略,目标140+

亲爱的同学们&#xff0c;寒假已经来临&#xff0c;春节即将到来&#xff0c;距离2024年上海高考已经余额不足5个月了。作为让许多学子头疼&#xff0c;也是拉分大户的数学科目&#xff0c;你准备好了吗&#xff1f;今天&#xff0c;六分成长为您分享上海高考数学最后四个多月的…

2024 高级前端面试题之 JS 「精选篇」

该内容主要整理关于 JS 的相关面试题&#xff0c;其他内容面试题请移步至 「最新最全的前端面试题集锦」 查看。 JS模块精选篇 1. 数据类型基础1.1 JS内置类型1.2 null和undefined区别1.3 null是对象吗&#xff1f;为什么&#xff1f;1.4 1.toString()为什么可以调用&#xff1…

燃烧的指针(三)

&#x1f308;个人主页&#xff1a;小田爱学编程 &#x1f525; 系列专栏&#xff1a;c语言从基础到进阶 &#x1f3c6;&#x1f3c6;关注博主&#xff0c;随时获取更多关于c语言的优质内容&#xff01;&#x1f3c6;&#x1f3c6; &#x1f600;欢迎来到小田代码世界~ &#x…

为什么需要使用线程池来创建线程?

当我们使用new Thread无限创建线程的时候 因为频繁的创建线程和销毁线程&#xff0c;cpu利用率会非常高 当cpu利用率达到100%的时候 那么没有可用的资源 让其他进程使用 那么其他进程访问就会导致卡顿 访问速度变慢 当我们使用线程池的时候 &#xff0c;cpu利用率就会降低&…

市场复盘总结 20240126

仅用于记录当天的市场情况&#xff0c;用于统计交易策略的适用情况&#xff0c;以便程序回测 短线核心&#xff1a;不参与任何级别的调整&#xff0c;采用龙空龙模式 昨日主题投资 连板进级率 27/105 25.7% 二进三&#xff1a; 进级率低 50% 最常用的二种方法&#xff1a; 方…

2024最新版Visual Studio Code安装使用指南

2024最新版Visual Studio Code安装使用指南 Installation and Usage Guide for the Latest Visual Studio Code in 2024 By JacksonML Visual Studio Code最新版1.85已经于2023年11月由其官网 https://code.visualstudio.com正式发布&#xff0c;这是微软公司2024年发行的的最…

vs2019报错MSB4019 找不到导入的项目“BuildCustomizations\CUDA 9.2.props”

在VS中执行生成&#xff0c;报错如下&#xff1a;严重性 代码 说明 项目 文件 行 禁止显示状态 错误 MSB4019 找不到导入的项目“D:\Microsoft Visual Studio\2019\Community\MSBuild\Microsoft\VC\v160\BuildCustomizations\CUDA 9.2.props”。请确认 Import 声明“D:\Microso…

Mybatis----分页

1.什么是分页 分页&#xff08;Pagination&#xff09;是指将大量数据划分为多个页面进行展示的一种技术手段。在数据量较大的情况下&#xff0c;将所有数据一次性显示在页面上会导致加载时间过长和页面过于庞大&#xff0c;影响用户体验和系统性能。分页技术通过划分数据为多…