一文看懂开源图化框架中的循环设计逻辑!

简介: 相信大家在日常工作中,已经精通各种循环逻辑的实现。就拿我来说吧,多年的工作经验,已经让我可以熟练的使用 C++,Python,英语等多种语言,循环多次输出“hello word”。不过大家有没有想过一个这样的问题:如何在一个有向无环图(Directed Acyclic Graph,简称dag)中实现循环呢?

大家好,我是不会写代码的纯序员——Chunel Feng。


相信大家在日常工作中,已经精通各种循环逻辑的实现。就拿我来说吧,多年的工作经验,已经让我可以熟练的使用 C++,Python,英语等多种语言,循环多次输出“hello word”。不过大家有没有想过一个这样的问题:如何在一个有向无环图(Directed Acyclic Graph,简称dag)中实现循环呢?


今天,我们就来介绍一下如何实现这个小目标 —— 而且是想怎么循环,就怎么循环哦~~~


功能介绍


为了防止有比我还小白的童鞋,我先几句话介绍一下什么是dagdag是一种图结构,由多个图节点组成,每个节点可以指向 0 个或 n 个其他的节点,且在图内部不会形成环形指向的结构。


如果觉得定义太枯燥,那看下面的图,哪个是dag、哪个不是dag应该就一目了然了。顺便说一句,tree结构也可以理解成是一种特殊的dag,就好比list可以理解成是一种特殊的tree一样。


38.png


在上一篇文章中,我们介绍了图(以下内容,dag和“图”表示同一概念)中节点是按什么顺序执行的。至于图中循环,举个例子,我们假设每个节点的功能,就是输出字母。这个时候,我们需要整个流图输出一串字母:abcbcd,需要如何实现?


最常规的方法:实现一些算子,功能是输出 a,bcbc,d 即可。 假设我要输出不同的 n 次 bc,还要实现 n 个不同的算子么?有人说,可以实现一个算子,输出 bc 即可,然后循环次数可以当做参数传递进来啊。但是这样的算子,功能不是最细粒度,可重用性并不高——比如下次又需要循环输出 ab 了呢?


还有一种方法:反正可以创建节点的,我们只要实现 4 个算子分别输出 a/b/c/d,然后向图里按照顺序注册 6 个次就好了。 那问题又来了,需要 bc 反复循环 100 次怎么办?又有方法:外部写个 for 循环,注册 100 次进去啊。emmm,好像也行。那如果是需要 bc 反复输出 100 亿次呢?for 循环注册 100 亿次?


注册节点实际上是在程序内部 new 节点的过程,是需要开辟内存和分配资源的。如果你的电脑性能这么给力的话,我不建议你用来写程序。应该直接去挖比特币,用比特币的钱再去买更多的电脑,再来挖更多的比特币——当然了,这又是另一种循环了,不在我们今天的讨论范围之内,哈哈。


39.png


言归正传,上图中,背景为蓝色的区域,表示需要循环执行的区域。可以看出,在dag中循环主要有四种形式:单点循环,多点串行循环,多点并行循环和多点混合循环。

【单点循环】就不多解释了,见名知意。


刚才抛出的问题,实际上是多点(b、c 两个节点)【串行循环】,循环的时候,bc 需要依次顺序执行。


多点【并行循环】,指的是输出的结果 bc 需要循环多次输出,但是可以无序的情况。

至于多点【混合循环】,可以理解成循环的区域又由好几部分组成,其中的部分区域需要串行,有的部分可以并行。如图第四部分所示,循环处执行的顺序是:b->cd(或 dc)->e。


B 节点明明仅依赖 A 节点,如何让程序执行完 C 节点之后,“掉头”回来继续执行 B 呢?如何标记这些信息呢?我们接下来就结合CGraph开源项目的实现逻辑,为大家做一些通用的解释。


需要申明的是,看懂以下内容并不需要你了解 CGraph 工程本身,但需要你已经了解了dag图的执行原理。还不懂执行原理的童鞋,可以先看一下我的上一篇文章:如何实现一个图化框架?代码已开源!


名词解释


我们先梳理几个名词,说到具体流程的时候,会用得到。


40.png


element(元素)


element 是所有被执行结构的基类,可以派生出node,group两种类型。无实际意义,且不可被执行。


node(节点)


node 是最小粒度的算子。node本身无法执行,但所有有具体功能的功能节点,都继承自node


functionNode(功能节点)


functionNode 是最小粒度的可执行算子,继承自node类,相当于是node的功能实现类。与node不同的是,functionNode有具体功能,且可以被执行。至于具体功能是什么,可以是输出字母 a,也可以是去挖比特币。总之,需要自己去实现。


group(组)


group 是多个functionNode的组合。自身不可以执行,但可以派生出clusterregion等组合逻辑。


cluster(簇)


cluster 继承自group,由多个functionNode线性组合而成。执行cluster的时候,内部的node依次顺序执行。简而言之就是可以依次完成多个功能。


region(区域)


region 继承自group,也是由多个functionNode组合而成。与cluster的区别是,region中的加入的node需要指定相互依赖关系。如果不指定依赖的话,就相当于是并发执行了,因为没有任何需要依赖信息。


pipeline(流水线)


pipeline是以上信息运行的地方。所有的functionNodeclusterregion信息,都需要注册到pipeline中,并且设定相互依赖关系。注册了以上三种信息的pipeline,实际上就对应了一个dag图,执行pipeline的过程,就是图执行的过程。


41.png


实现思路


一句话:分治!


看了上面介绍的朋友,应该已经能想到,一个图实际上是由多个不同的子模块组成。这些子模块都可以拆解成functionNodeclusterregion这三种形式,而这三种形式都统一派生自element类。在向图中注册element的时候,也可以注册这个element的循环执行次数。而不同的element,又有自己专属的执行方式,比如:functionNode就是简单执行自己的功能,而cluster就是依次执行其中的functionNode。针对cluster的循环执行,就是依次执行其中的functionNode,并且循环 n 次即可。


当然,还有更扫的。functionNodeclusterregion这三个类,实际都继承自element,那相当于是cluster中,不仅可以加入functionNode,也可以加入region甚至是另一个cluster了。而加入的信息中,又可以有自己的循环执行逻辑。region也是同理,这样就可以实现cluster中加入循环 n 次的regionregion中再加入循环 m 次的内部的cluster的无限套娃逻辑了。


当然了,针对循环问题,我相信 bfs 或者 dfs 应该也是可以解决的。但是解决的过程,会相对更加吃力一些,而且流程的颗粒度没有使用分治来的清晰。有兴趣的朋友,可以自己尝试一下。


42.png


给大家带上几句 CGraph 中具体实现的代码吧。更多的例子,可以去 github 去搜索同名工程,里面还有实现了套娃逻辑的例子哦,哈哈。


#include "MyGNode/MyNode1.h"#include "MyGNode/MyNode2.h"void tutorial_cluster () {    CSTATUS status = STATUS_OK;    GPipelinePtr pipeline = new GPipeline();    GElementPtr a, b_cluster, c, d = nullptr;    b_cluster = pipeline->createGGroup<GCluster>({         pipeline->createGNode<MyNode1>(GNodeInfo("nodeB1", 1)),    // 创建名为nodeB1的node信息,并将其放入b_cluster中         pipeline->createGNode<MyNode1>(GNodeInfo("nodeB2", 3)),    // 创建名为nodeB2且自循环3次的node信息,并将其放入b_cluster中         pipeline->createGNode<MyNode2>(GNodeInfo("nodeB3", 1))    });    // 创建cluster信息,包含了三个node信息    /* 请对所有返回值进行判定 */    status = pipeline->registerGElement<MyNode1>(&a, {}, "nodeA", 1);        // 将名为nodeA的node信息,注册入pipeline中    status = pipeline->registerGElement<GCluster>(&b_cluster, {a}, "clusterB", 2);    // 将名为clusterB,依赖a执行且自循环2次的cluster信息,注册入pipeline中    status = pipeline->registerGElement<MyNode1>(&c, {a}, "nodeC", 1);    status = pipeline->registerGElement<MyNode2>(&d, {b_cluster, c}, "nodeD", 2);    status = pipeline->process();    CGRAPH_ECHO("pipeline process status is : [%d]", status);    delete pipeline;}


在 CGraph 中,以上几句代码,就实现了图片中描述的循环逻辑。其中,MyNode1MyNode2是继承于node并且实现了特定功能的子类。AB1C等信息,都是一个functionNode。而B1B2B3作为一个cluster,被循环执行 2 次。每个循环的过程中,B2又自己单独循环执行 3 次,D自己循环执行 2 次。


假设ACD的功能是打印字母 A、C、D,而B1B2B3的功能是打印数字 1、2、3。那最终打印出来的结果,就是 A[1C222312223]DD,其中长串数字和 C 的输出顺序(中括号内部)每次可能不同,因为是并行执行。


写在最后


大数据和人工智能当道的时代,图化框架已然成了当下的主流。对这部分知识的了解,是很有必要加入日程的。大家熟悉的 tensorflow,flink 等库,在数据传递和计算的时候,底层都维护了一张“看不见的图”。


CGraph 项目是一个 C++的开源的并行图计算框架,最近也是刚刚起步,还有很多功能没有实现,比如跨模块参数的传递、底层并发优化、当前信息快照和异常恢复处理等等等。也希望有兴趣的朋友可以联系我们,加入我们,一起实现一点小的功能,或者测出几个 bug 啥的。生而为程序员,一起为开(bai)源(piao)社区做一点自己的贡献,提升自己的同时,也可以做到 build together, power another —— 共建,赋能。


引用链接


[1] 一面之猿网: http://www.chunel.cn/

[2] CGraph 图计算框架: https://github.com/ChunelFeng/CGraph


目录
相关文章
|
7月前
|
C语言 C++ 开发者
深入探索C++:特性、代码实践及流程图解析
深入探索C++:特性、代码实践及流程图解析
|
4月前
|
SQL Java 数据库
建模底层逻辑问题之ORM框架建模中,执行SQL的过程中被抽象和组织是如何实现的
建模底层逻辑问题之ORM框架建模中,执行SQL的过程中被抽象和组织是如何实现的
|
4月前
|
机器学习/深度学习 分布式计算 前端开发
构建前端防腐策略问题之前端代码会随着技术引擎的迭代而腐烂的问题如何解决
构建前端防腐策略问题之前端代码会随着技术引擎的迭代而腐烂的问题如何解决
|
6月前
|
SQL 存储 缓存
第四章 逻辑架构(1)
第四章 逻辑架构
41 1
|
6月前
|
SQL 存储 缓存
第四章 逻辑架构(2)
第四章 逻辑架构
36 1
|
7月前
|
SQL 设计模式 Java
【软件工程底层逻辑系列】建模的底层逻辑
在本文中,给出建模的底层逻辑:用图形逻辑地表达现实业务的抽象,通过一些大家通识的技术案例讲述建模的过程。
75001 3
|
Go 开发者
一文详解Go语言接口嵌套组合的精髓!
一文详解Go语言接口嵌套组合的精髓!
206 0
|
机器学习/深度学习 人工智能 自然语言处理
这 10 本书,带你了解 ChatGPT 的底层逻辑
作为一门应用型学科,机器学习植根于数学理论,落地于代码实现。这就意味着,掌握公式推导和代码编写,方能更加深入地理解机器学习算法的内在逻辑和运行机制。 本书在对全部机器学习算法进行分类梳理的基础上,分别对监督学习单模型、监督学习集成模型、无监督学习模型、概率模型四个大类共 26 个经典算法进行了细致的公式推导和代码实现,旨在帮助机器学习的学习者和研究者完整地掌握算法细节、实现方法以及内在逻辑。
248 0
|
存储 自然语言处理 算法
GaiaX开源解读 | 表达式作为逻辑动态化的基础,我们是如何设计的
GaiaX跨端模板引擎,是在阿里优酷、淘票票、大麦内广泛使用的Native动态化方案,其核心优势是性能、稳定和易用。本系列文章《GaiaX开源解读》,带大家看看过去三年GaiaX的发展过程。
367 0
|
存储 自然语言处理 算法
作为逻辑动态化的基础,GaiaX 表达式是如何设计的? | GaiaX 开源解读
GaiaX 跨端模板引擎,是在阿里文娱内广泛使用的 Native 动态化方案,其核心优势是性能、稳定和易用。本系列文章《GaiaX 开源解读》,带大家看看过去三年 GaiaX 的发展过程。 GaiaX 开源地址:https://github.com/alibaba/GaiaX
432 0
作为逻辑动态化的基础,GaiaX 表达式是如何设计的? | GaiaX 开源解读
下一篇
DataWorks