多线程任务调度系统


ThreadLoom:用 C++17 写一个优先级任务调度器
github地址:https://github.com/johnjiamzhong-project/ThreadLoom

前言

最近用 C++17 从零实现了一个多线程任务调度库——ThreadLoom(线程织机)。名字取"用线程编织任务"之意。这篇文章记录整个设计思路,顺带聊聊实现中几个值得注意的细节。

项目地址:(GitHub 链接可在这里填)


整体架构

            submit()

调用方线程 ─────────────► 优先级队列 (pq_)

dispatcher_ 线程(唯一消费者)

ThreadPool(N 个工作线程)

task->execute()

三层结构,职责清晰:

┌────────┬─────────────────────┬──────────────────────────────────────────┐
│ 层 │ 类 │ 职责 │
├────────┼─────────────────────┼──────────────────────────────────────────┤
│ 任务层 | Task / FunctionTask │ 封装可执行逻辑,持有状态、优先级、ID |
├────────┼─────────────────────┼──────────────────────────────────────────┤
│ 调度层 │ Scheduler │ 优先级排序,FIFO 保序,dispatch 线程驱动 │
├────────┼─────────────────────┼──────────────────────────────────────────┤
│ 执行层 │ ThreadPool │ 固定线程数,FIFO 工作队列,空闲检测 │
└────────┴─────────────────────┴──────────────────────────────────────────┘


核心设计决策

  1. 优先级 + FIFO 双重排序

优先级队列里的元素是 PriorityEntry,除了任务指针还带两个字段:

  struct PriorityEntry {
      int priority;     // Critical=3, High=2, Normal=1, Low=0
      uint64_t seqNum;  // 全局递增提交序号
      std::unique_ptr<Task> task;

      bool operator<(const PriorityEntry& o) const {
          if (priority != o.priority)
              return priority < o.priority;  // priority 大的先出
          return seqNum > o.seqNum;          // seqNum 小的先出(FIFO)
      }
  };

std::priority_queue 是最大堆,operator< 返回 true 意味着"我比对方优先级低"。这里有一个反直觉的地方:priority 大的应该先出队,所以当 priority < o.priority 时返回 true(我比对方"小",排在后面)。同优先级时
seqNum 小的更早提交,所以同理 seqNum > o.seqNum 时返回 true。

  1. 调度线程与线程池分离

很多简单实现里,提交任务直接塞进线程池队列,没有优先级可言。ThreadLoom 在线程池前面加了一个专属 dispatcher_
线程,它是优先级队列的唯一消费者,确保"每次只取优先级最高的任务"再交给线程池。线程池本身只是无差别的 FIFO 执行器,不感知优先级。

  1. 任务状态用原子量

std::atomic state_;

cancel() 通过 CAS 保证只有 Pending 状态能转为 Cancelled,避免已经开始执行的任务被错误取消:

  void Task::cancel() {
      TaskState expected = TaskState::Pending;
      state_.compare_exchange_strong(expected, TaskState::Cancelled);
  }

运行中的任务需要在 execute() 内主动调用 isCancelled() 来配合提前退出,协作式取消。

  1. waitAll() 的实现

等待"队列为空 且 线程池全部空闲",两个条件缺一不可,不然会提前返回:

Scheduler::waitAll()
→ 等待 pq_ 为空
→ ThreadPool::waitIdle()
→ 等待 queue_ 为空 且 activeCount_ == 0

activeCount_ 在工作线程取任务时 +1、执行完后 -1,配合 idleCv_ 条件变量通知 waitIdle()。


使用示例

  Scheduler scheduler(4); // 4 个工作线程

  // 提交 lambda 任务
  scheduler.submit("task-name", [] {
      // 你的逻辑
  }, TaskPriority::High);

  // 提交自定义任务(继承 Task 并实现 execute())
  scheduler.submit(std::make_unique<MyTask>("my-task", TaskPriority::Critical));

  scheduler.start();   // 开始调度
  scheduler.waitAll(); // 等待全部完成

Demo 输出(9 个不同优先级任务,Critical 最先执行):

  === ThreadLoom Scheduler Demo ===

  Submitted 9 tasks. Waiting for completion...

  [FnTask] Critical priority #1
  [FnTask] High priority #1
  [Task 7] compute-A  =>  7^2 = 49
  [FnTask] Normal priority #1
  [FnTask] Normal priority #2
  [Task 8] compute-B  =>  13^2 = 169
  [FnTask] Low priority #1
  [FnTask] Low priority #2
  [Task 9] compute-C  =>  42^2 = 1764

  === All tasks completed. Total: 9 ===

当前局限与后续计划

坦白说,这是一个扎实的基础版本,还有几个明显的 gap:

  • 任务状态未全程跟踪:Running / Completed / Failed 状态字段存在,但调度器还没在执行前后更新它
  • 无异常处理:execute() 抛异常会导致工作线程崩溃
  • Scheduler 没有 cancel(id) 接口:Task 层支持取消,但调度器没有暴露按 ID 取消的 API
  • 没有 Future/结果回传:目前任务是 fire-and-forget,无法拿到返回值
  • 零测试覆盖:后续补 Google Test

后续考虑加延迟任务、动态调整线程数、任务依赖(B 等 A 完成后执行)等特性。


小结

这个项目的价值不在于"又造了个轮子",而在于亲手把调度器里那些教科书级的概念——优先级队列、条件变量、原子操作、协作式取消——串联成一个真实可运行的系统。代码量不大(核心约 400
行)。感兴趣的可以看源码或在评论区交流。


▎ 技术栈:C++17 / CMake / MSVC
▎ 平台:Windows,跨平台理论可行(std::thread 标准库)

posted @ 2026-04-17 21:55  rambos1996  阅读(10)  评论(0)    收藏  举报