编码母题 / P1 / 异步编程 / 03

按依赖关系执行的任务调度器

先读清题意与边界,独立尝试,再展开思路并对照可运行的参考代码。

重要程度高价值难度困难预计完成50–70 分钟

难度按 LeetCode 简单/中等/困难三档估计;工程题并非 LeetCode 官方题目。用时指独立编码并检查主要边界的参考时间,因经验而异。

题目是什么

任务有 id、依赖列表和 run 函数。先检查缺失依赖、重复 id 和环,再在并发上限内只启动依赖全部成功的任务;失败后不再启动新任务。

输入输出示例

A 完成后 B、C 可并行,D 同时依赖 B、C 时须等待两者都完成。

练习起手式与验证

复制代码后补全空方法,再运行下方三组验证;复杂题所需的辅助类型与输入准备一并放在代码块中。

/*
 * 题目:按依赖关系执行的任务调度器
 * 要求:任务有 id、依赖列表和 run 函数。先检查缺失依赖、重复 id 和环,
 * 再在并发上限内只启动依赖全部成功的任务;失败后不再启动新任务。
 * 示例:A 完成后 B、C 可并行,D 同时依赖 B、C 时须等待两者都完成。
 */
function validateGraph(tasks) {}

function scheduleTasks(tasks, concurrency) {}

(async () => {
  const order = [];
  const results = await scheduleTasks(
    [
      {
        id: "a",
        dependencies: [],
        run: async () => {
          order.push("a");
          return 2;
        },
      },
      {
        id: "b",
        dependencies: ["a"],
        run: (values) => {
          order.push("b");
          return values.get("a") + 1;
        },
      },
      {
        id: "c",
        dependencies: ["a"],
        run: () => {
          order.push("c");
          return 4;
        },
      },
      {
        id: "d",
        dependencies: ["b", "c"],
        run: () => {
          order.push("d");
          return 5;
        },
      },
    ],
    2,
  );
  console.log(results.get("d")); // 预期:5
  console.log(order.indexOf("a") < order.indexOf("b")); // 预期:true
  console.log(order.indexOf("a") < order.indexOf("c")); // 预期:true
})().catch(console.error);

解题思路

展开思路、不变量与常见边界

关键切入点:先拓扑验证 DAG,运行时维护剩余依赖数与 ready 队列,并用 pump 补满并发槽位。

核心模型:先用 Kahn 拓扑排序验证 DAG;运行时维护剩余依赖数和 ready FIFO。 固定 Worker 从 ready 队列领取任务,完成后降低后继入度并唤醒等待 Worker。 为避免复杂条件变量,本实现用事件驱动的 pump 调度:每次完成后补满并发槽位。 不变量:任务只启动一次;启动时所有依赖均成功;active <= concurrency; completed + active + ready/blocked = 总任务数。时间和空间复杂度均为 O(V+E)。 迁移:构建系统、工作流、Agent Plan 执行、数据处理 Pipeline。

参考代码与验证

“关键机制”注释用于解释实现,下载链接提供未经页面注释修改的原始 JS 文件。

// 关键机制:先拓扑验证 DAG,运行时维护剩余依赖数与 ready 队列,并用 pump 补满并发槽位。
function validateGraph(tasks) {
  const byId = new Map();
  for (const task of tasks) {
    if (!task || typeof task.id !== "string" || typeof task.run !== "function")
      throw new TypeError("invalid task");
    if (byId.has(task.id)) throw new Error(`duplicate task id: ${task.id}`);
    byId.set(task.id, task);
  }
  const indegree = new Map();
  const successors = new Map(tasks.map((task) => [task.id, []]));
  for (const task of tasks) {
    const dependencies = task.dependencies || [];
    const unique = new Set(dependencies);
    if (unique.size !== dependencies.length)
      throw new Error(`duplicate dependency in ${task.id}`);
    indegree.set(task.id, dependencies.length);
    for (const dependency of dependencies) {
      if (!byId.has(dependency))
        throw new Error(`unknown dependency: ${dependency}`);
      successors.get(dependency).push(task.id);
    }
  }
  const copy = new Map(indegree);
  const queue = tasks
    .filter((task) => copy.get(task.id) === 0)
    .map((task) => task.id);
  let visited = 0;
  for (let head = 0; head < queue.length; head += 1) {
    const id = queue[head];
    visited += 1;
    for (const next of successors.get(id)) {
      copy.set(next, copy.get(next) - 1);
      if (copy.get(next) === 0) queue.push(next);
    }
  }
  if (visited !== tasks.length) throw new Error("dependency cycle detected");
  return { byId, indegree, successors };
}

function scheduleTasks(tasks, concurrency) {
  if (!Array.isArray(tasks))
    return Promise.reject(new TypeError("tasks must be an array"));
  if (!Number.isInteger(concurrency) || concurrency < 1)
    return Promise.reject(new RangeError("invalid concurrency"));
  let graph;
  try {
    graph = validateGraph(tasks);
  } catch (error) {
    return Promise.reject(error);
  }
  if (!tasks.length) return Promise.resolve(new Map());
  const ready = tasks
    .filter((task) => graph.indegree.get(task.id) === 0)
    .map((task) => task.id);
  let readyHead = 0;
  const results = new Map();
  let active = 0;
  let completed = 0;
  let stopped = false;

  return new Promise((resolve, reject) => {
    function pump() {
      while (!stopped && active < concurrency && readyHead < ready.length) {
        const id = ready[readyHead++];
        active += 1;
        Promise.resolve()
          .then(() => graph.byId.get(id).run(results))
          .then((value) => {
            results.set(id, value);
            completed += 1;
            for (const next of graph.successors.get(id)) {
              graph.indegree.set(next, graph.indegree.get(next) - 1);
              if (graph.indegree.get(next) === 0) ready.push(next);
            }
          })
          .catch((error) => {
            stopped = true;
            reject(error);
          })
          .finally(() => {
            active -= 1;
            if (stopped) return;
            if (completed === tasks.length) resolve(results);
            else pump();
          });
      }
    }
    pump();
  });
}

// 三组以上可直接运行的验证;异步题使用独立执行块。
(async () => {
  const order = [];
  const results = await scheduleTasks(
    [
      {
        id: "a",
        dependencies: [],
        run: async () => {
          order.push("a");
          return 2;
        },
      },
      {
        id: "b",
        dependencies: ["a"],
        run: (values) => {
          order.push("b");
          return values.get("a") + 1;
        },
      },
      {
        id: "c",
        dependencies: ["a"],
        run: () => {
          order.push("c");
          return 4;
        },
      },
      {
        id: "d",
        dependencies: ["b", "c"],
        run: () => {
          order.push("d");
          return 5;
        },
      },
    ],
    2,
  );
  console.log(results.get("d")); // 预期:5
  console.log(order.indexOf("a") < order.indexOf("b")); // 预期:true
  console.log(order.indexOf("a") < order.indexOf("c")); // 预期:true
  console.log(order.indexOf("d") > order.indexOf("b")); // 预期:true
  console.log(await Promise.resolve(scheduleTasks([{ id: "x", dependencies: ["x"], run() {} }], 1)).then(() => false, () => true)); // 预期:true(拒绝)
  const independent = Array.from({ length: 1000 }, (_, i) => ({
    id: String(i),
    dependencies: [],
    run: () => i,
  }));
  console.log((await scheduleTasks(independent, 8)).size); // 预期:independent.length


})().catch(console.error);

复杂度分析

时间复杂度:O(V+E),不计任务本身。建图、验环与运行调度各扫描任务及依赖有限次。

空间复杂度:O(V+E)。图、入度、就绪队列和结果表保存任务及依赖。

下载原始 JS 文件