DAG以及任务调度

本文介绍了有向无环图(DAG)的概念及其在任务调度中的应用。阐述了图的遍历方法,特别是拓扑排序,以及如何在DAG中寻找入度为0的顶点进行遍历。此外,还探讨了任务调度组件如何利用DAG来管理任务的执行顺序,确保任务按照依赖关系正确执行。

有向无环图

  图的遍历是指从图中的某一个顶点出发,按照某种搜索方法沿着图中的边对图中的所有顶点访问一次且仅访问一次。图的遍历主要有两种算法:BFS和DFS。
  对于任何有向无环图(DAG)而言,其拓扑排序为其所有结点的一个线性排序(同一个有向图可能存在多个这样的结点排序)。该排序满足这样的条件——对于图中的任意两个结点U和V,若存在一条有向边从U指向V,则在拓扑排序中U一定出现在V前面。通俗来讲:拓扑排序是一个有向无环图(DAG)的所有顶点的线性序列,该序列必须满足两个条件:

  1. 每个顶点出现且只出现一次
  2. 若存在一条从顶点A到顶点B的路径,那么在序列中顶点 A出现在顶点 B的前面

寻找出DAG的拓扑排序

  1. 从DAG图中选择一个入度为0的顶点并输出
  2. 从图中删除该顶点和所有以它为起点的有向边
  3. 重复1和2直到当前的DAG图为空或当前图中不存在入度为0的顶点为止。后一种情况说明有向图中必然存在环

  假设有向图中不存在起点和终点为同一结点的有向边。有向图结点的入度(indegree)和出度(outdegree)的概念:
  入度:设有向图中有一结点V,其入度即为当前所有从其他结点出发,终点为V的的边的数目。也就是所有指向V的有向边的数目。
  出度:设有向图中有一结点V,其出度即为当前所有起点为V,指向其他结点的边的数目。也就是所有由V发出的边的数目。

  与普通的广度优先遍历可以从该DAG任意一个结点开始遍历不同,这里应当保存每一个结点对应的入度,并在遍历的每一层选取入度为0的结点开始遍历。下面给出广度优先遍历拓扑排序的代码:

算法描述:
初始化一个Map或者类似数据结构来保存每一个结点的入度。
对于图中的每一个结点的子结点,将其子结点的入度加1。
选取入度为0的任意一个结点开始遍历,并将该节点加入输出。
对于遍历过的每个结点,更新其子结点的入度:将子结点的入度减1。
重复步骤3,直到遍历完所有的结点。
如果无法遍历完所有的结点,则意味着当前的图不是有向无环图。不存在拓扑排序。

public class TopologicalSort {
  /**
   * 判断是否有环及拓扑排序结果
   *
   * 有向无环图(DAG)才有拓扑(topological)排序
   * 广度优先遍历的主要做法:
   *    1、遍历图中所有的顶点,将入度为0的顶点入队列。
   *    2、从队列中poll出一个顶点,更新该顶点的邻接点的入度(减1),如果邻接点的入度减1之后等于0,则将该邻接点入队列。
   *    3、一直执行第2步,直到队列为空。
   * 如果无法遍历完所有的结点,则意味着当前的图不是有向无环图。不存在拓扑排序。
   *
   *
   * @return key返回的是状态, 如果成功(无环)为true, 失败则有环, value为拓扑排序结果(可能是其中一种)
   */
  private Map.Entry<Boolean, List<Vertex>> topologicalSort() {
	//入度为0的结点队列
    Queue<Vertex> zeroIndegreeVertexQueue = new LinkedList<>();
    //保存结果
    List<Vertex> topoResultList = new ArrayList<>();
    //保存入度不为0的结点
    Map<Vertex, Integer> notZeroIndegreeVertexMap = new HashMap<>();
    //扫描所有的顶点,将入度为0的顶点入队列
    for (Map.Entry<Vertex, VertexInfo> vertices : verticesMap.entrySet()) {
      Vertex vertex = vertices.getKey();
      int inDegree = getIndegree(vertex);

      if (inDegree == 0) {
        zeroIndegreeVertexQueue.add(vertex);
        topoResultList.add(vertex);
      } else {
        notZeroIndegreeVertexMap.put(vertex, inDegree);
      }
    }
	//扫描完后,没有入度为0的结点,说明有环,直接返回
    if(zeroIndegreeVertexQueue.isEmpty()){
      return new AbstractMap.SimpleEntry(false, topoResultList);
    }
    //采用topology算法, 删除入度为0的结点和它的关联边
    while (!zeroIndegreeVertexQueue.isEmpty()) {
      Vertex v = zeroIndegreeVertexQueue.poll();
      //得到相邻结点
      Set<Vertex> subsequentNodes = getSubsequentNodes(v);
      for (Vertex subsequentVertex : subsequentNodes) {
        Integer degree = notZeroIndegreeVertexMap.get(subsequentVertex);
        if(--degree == 0){
          topoResultList.add(subsequentVertex);
          zeroIndegreeVertexQueue.add(subsequentVertex);
          notZeroIndegreeVertexMap.remove(subsequentVertex);
        }else{
          notZeroIndegreeVertexMap.put(subsequentVertex, degree);
        }
      }
    }
    //notZeroIndegreeVertexMap如果为空, 表示没有环
    AbstractMap.SimpleEntry resultMap = new AbstractMap.SimpleEntry(notZeroIndegreeVertexMap.size() == 0 , topoResultList);
    return resultMap;
  }
}

注意输出结果是该图的拓扑排序序列之一。
每次在入度为0的集合中取顶点,并没有特殊的取出规则,取顶点的顺序不同会得到不同的拓扑排序序列(如果该图有多种排序序列)。

由于输出每个顶点的同时还要删除以它为起点的边。如果图有V个顶点,E条边,则一般该算法的时间复杂度为O(V+E)。这里实现的算法最终key返回的是状态, 如果成功(无环)为true, 失败则有环, 无环时value为拓扑排序结果(可能是其中一种)。

任务调度

任务调度组件的核心使命是让任务按照既定的执行计划去执行。对于复杂的任务,是由多个任务组成一个任务组,它们之间存在依赖关系,一个任务执行的条件,必须是它的前置任务已经执行成功(或者没有前置任务),它才可以执行。
在这里插入图片描述
这个任务关系图就是“有向无环图”(DAG)。图是由一系列顶点和连接顶点的边组成的数据结构。它分为有向图和无向图。有向图的边是有方向的,即A->B这条边和B->A是两条不同的边,而无向图中,A->B和B->A是共用一条边的。基于这种数据结构,可以用图的顶点表示一个任务,而图的边表示任务之间的依赖关系,就可以基于有向无环图来实现任务调度。下面基于DAG实现一个任务调度系统。

//定义一个Executor接口
//代表一个可执行的任务,execute代表任务的执行
public interface Executor {
    boolean execute();
}
//定义一个Executor接口的实现Task
public class Task implements Executor{
    private Long id;
    private String name;
    private int state;
 
    public Task(Long id, String name, int state) {
        this.id = id;
        this.name = name;
        this.state = state;
    }
 
    public boolean execute() {
        System.out.println("Task id: [" + id + "], " + "task name: [" + name +"] is running");
        state = 1;
        return true;
    }
 
    public boolean hasExecuted() {
        return state == 1;
    }
}
/*
*id:任务id
*name:任务名
*state:任务状态,简化为0:未执行,1:已执行
*hasExecuted返回任务是否已执行
*/
//任务图
public class Digraph {
    private Set<Task> tasks;
    private Map<Task, Set<Task>> map;
 
    public Digraph() {
        this.tasks = new HashSet<Task>();
        this.map = new HashMap<Task, Set<Task>>();
    }
 
    public void addEdge(Task task, Task prev) {
        if (!tasks.contains(task) || !tasks.contains(prev)) {
            throw new IllegalArgumentException();
        }
        Set<Task> prevs = map.get(task);
        if (prevs == null) {
            prevs = new HashSet<Task>();
            map.put(task, prevs);
        }
        if (prevs.contains(prev)) {
            throw new IllegalArgumentException();
        }
        prevs.add(prev);
    }
 
    public void addTask(Task task) {
        if (tasks.contains(task)) {
            throw new IllegalArgumentException();
        }
        tasks.add(task);
    }
 
    public void remove(Task task) {
        if (!tasks.contains(task)) {
            return;
        }
        if (map.containsKey(task)) {
            map.remove(task);
        }
        for (Set<Task> set : map.values()) {
            if (set.contains(task)) {
                set.remove(task);
            }
        }
    }
 
    public Set<Task> getTasks() {
        return tasks;
    }
 
    public void setTasks(Set<Task> tasks) {
        this.tasks = tasks;
    }
 
    public Map<Task, Set<Task>> getMap() {
        return map;
    }
 
    public void setMap(Map<Task, Set<Task>> map) {
        this.map = map;
    }
}

//这个类使用了邻接表来表示有向无环图。tasks是顶点集合,也就是任务集合。
//map是任务依赖关系集合。key是一个任务,value是它的前置任务集合。
//一个任务执行的前提是它在map中没有以它作为key的entry,或者是它的前置任务集合中的任务都是已执行的状态。
//调度器
public class Scheduler {
    public void schedule(Digraph digraph) {
        while (true) {
            List<Task> todo = new ArrayList<Task>();
            for (Task task : digraph.getTasks()) {
                if (!task.hasExecuted()) {
                    Set<Task> prevs = digraph.getMap().get(task);
                    if (prevs != null && !prevs.isEmpty()) {
                        boolean toAdd = true;
                        for (Task task1 : prevs) {
                            if (!task1.hasExecuted()) {
                                toAdd = false;
                                break;
                            }
                        }
                        if (toAdd) {
                            todo.add(task);
                        }
                    } else {
                        todo.add(task);
                    }
                }
            }
            if (!todo.isEmpty()) {
                for (Task task : todo) {
                    if (!task.execute()) {
                        throw new RuntimeException();
                    }
                }
            } else {
                break;
            }
        }
    }
 
    public static void main(String[] args) {
        Digraph digraph = new Digraph();
        Task task1 = new Task(1L, "task1", 0);
        Task task2 = new Task(2L, "task2", 0);
        Task task3 = new Task(3L, "task3", 0);
        Task task4 = new Task(4L, "task4", 0);
        Task task5 = new Task(5L, "task5", 0);
        Task task6 = new Task(6L, "task6", 0);
        digraph.addTask(task1);
        digraph.addTask(task2);
        digraph.addTask(task3);
        digraph.addTask(task4);
        digraph.addTask(task5);
        digraph.addTask(task6);
        digraph.addEdge(task1, task2);
        digraph.addEdge(task1, task5);
        digraph.addEdge(task6, task2);
        digraph.addEdge(task2, task3);
        digraph.addEdge(task2, task4);
        Scheduler scheduler = new Scheduler();
        scheduler.schedule(digraph);
    }
}
//调度器的实现比较简单,就是遍历任务集合,找出待执行的任务集合,
//放到一个List中,再串行执行(若考虑性能,可优化为并行执行)。
//若List为空,说明所有任务都已执行,则这一次任务调度结束。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值