在开始之前,我首先要提到的是术语“图形”,我将参考显示结构的图像。由于我的视力障碍,我既无法想象也无法画出它们。我可以看着他们并理解 - 但我很难自己制作它们。
我正在开发一个使用脚本语言生成目标的构建工具,这些目标经过处理并分成任务。任务表示为数据结构:https://github.com/IngwiePhoenix/IceTea/blob/master/src/main.cpp#L98
正如您所看到的,Task
类注意到它是一个master(该过程的实际结果)并将一个引用 - 注释,仅一个引用而不是数组 - 存储到其子级和父级 - 如果有的话。
到目前为止,我可以使用所有必需的任务填充任务队列,并将其发送到执行者void Run(void*)
:https://github.com/IngwiePhoenix/IceTea/blob/master/src/main.cpp#L1107
但问题出现了:
任务未正确排序,如果先前的构建被取消然后再次启动,构建将变得一团糟。如果您有a.cpp
和b.cpp
并且您的上一个构建在两个中的任何一个上终止而另一个未构建,则会导致父任务被运行两次。一个适当的排序机制 - 一个拓扑机制 - 很可能解决这个问题。目前,我已将此归类为不存在的依赖关系跟踪。
当一个目标依赖另一个目标时,目标取决于另一个目标可能最终会在队列之前依赖于该目标。想象一下,您有libfoo.a
和bar.exe
。可能会发生,编译bar.exe
的任务在创建libfoo.a
的任务之前。这意味着,我们遇到了链接错误。
目前,该程序由一个线程池构成,该线程池从队列中提取任务并运行它们。但在我对Ninja工具进行了更多研究后,这种逻辑可能会发生变化;将有一个线程池只执行从脚本语言生成的命令。但是,现在必须等待。因此,我只在一个线程上运行该工具,以模拟我真正想要的行为;一个命令在另一个命令之后运行。
我主要面临的问题仍然是排序机制。从我对构建工具的研究中,他们倾向于使用所谓的DAG和拓扑排序。现在,我既没有找到关于如何编写拓扑排序算法的良好解释,也没有弄清楚它是如何工作的。我知道有常量u
和v
。但我无法找到实现它的方法。
到目前为止,我已经明白这是一个线性图。我们假设以下结构:
myApp.exe
- main.o
- util.o
| libfoo.a
- foo_a.o
- foo_b.o
这非常直截了当。有一个结果,一个依赖。但是如果我们有两个结果取决于相同的libfoo呢?
myApp.exe libbar.so
- main.o - barlib.o
- util.o
-------------------------
| libfoo.a
- foo_a.o
- foo_b.o
这就是我已经陷入困境的地方。
您是否可以向我解释如何实现拓扑算法,以及DAG实际上是什么?这将非常有帮助,因为我非常诚实地在这里遇到一个难以克服的障碍。提前谢谢!
附注:我希望尽可能小地保留工具,因此我无法添加像Boost.Graph这样的东西,我将其视为搜索结果。
答案 0 :(得分:1)
好的,首先要做的事情是:DAG是 D 竖立的 A 循环 G raph。
图表是具有连接的节点的数据结构 某种方式。例如,地图是交叉点所在的图形 节点和道路是边缘。
有向图是边缘有某种方向的图形; 例如,许多道路是无向的(请保持正确 路边),但有些是单向的。
非循环图是没有任何循环的图;这意味着一次 你离开一个节点,你就无法回到它。
您无法对循环图进行排序;周期的哪一部分会先来?但是你可以对非循环图进行排序,结果是“拓扑排序”。正如您在第二个示例中指出的(可能是偶然的),多个拓扑排序可能对同一个图形有效。
为了处理问题中的依赖关系,您将 使用图表。但是,既然你知道它将是非循环的,你可以编写自己的,非常简洁的图类。
struct node {
string name;
vector<node*> out_neighbors;
vector<node*> in_neighbors;
}
您将所有节点存储在单个数组中graph
,并且节点会在out_neighbors
中保留它们之后的节点列表(这称为“邻接列表”格式)
现在你如何对它们进行排序?好吧,您希望对它们进行排序,以便节点不依赖于稍后出现的节点。首先,您希望找到没有传入边缘的所有节点(即,不依赖于任何东西)。
queue<node*> free;
for(int i=0; i<n; ++i)
if(graph[i].in_neighbors.size() == 0)
free.push(&graph[i]);
接下来,您将使用这些来查找其他节点。
queue<node*> topsort; //this is going to be the sorted nodes
while(free.size() > 0) {
node* top = free.front(); //remove the first element from the graph
topsort.push(top);
free.pop();
for(int i=0; i<top->out_neighbors.size(); ++i) {
node* neighbor = top->out_neighbors[i];
neighbor->in_neighbors.erase(
find(neighbor->in_neighbors.begin(), neighbor->in_neighbors.end(), top)
);
if(neighbor->in_neighbors.size() == 0)
free.push(neighbor);
}
}
最后,topsort将是排序拓扑顺序中的节点指针列表。但是,您的所有传入边缘也已从图形中删除(这可以使用传出边缘轻松重建)。
一些建议:
首先,你的Task
已经像我在这里描述的简单结构,除了它们只有一个父指针和子指针;为什么没有父母(传入)和孩子(传出)的列表?然后,您可以对Task
个对象进行排序。
其次,一旦它在拓扑上排序,你就可以在线程中运行;只要我的所有传入边缘都已编译完毕,我也可以随意使用。