数据结构课设实战:手写任务调度器,从能跑到可验收 简介这份资源是大二数据结构课程设计的完整工程包围绕基于 C/C 的任务调度器系统设计与实现展开面向正在完成数据结构课设、需要参考可运行项目结构与调度算法落地的本科生。压缩包共 38 个文件约 40.97MB以 Visual Studio 工程文件sln、vcxproj、filters、C 源码与头文件cpp、hpp、编译中间产物obj、pdb、tlog、ipch以及可执行文件为主另含类图与说明文本解压后可直接用 VS2019 打开工程文件运行。已有 207 人学习下载。资源价值在于提供了一套包运行通过的调度器实现范例读者可从中了解任务队列组织、调度策略编码与多文件工程拆分方式并借助类图与头文件快速理清模块职责适合作为课设参考、代码对照与调试排错的起点。1. 从“能跑就行”到“可验收”任务调度器课程设计到底要交什么很多同学拿到“数据结构课程设计”这道题第一反应是去网上找一段能跑的代码改改输出就交。结果答辩时被问一句“你的优先队列为什么用堆不用有序数组”当场卡壳。任务调度器这个题目看着简单实际上把线性表、栈、队列、优先队列、图、排序、查找全串了一遍是数据结构课设里最能体现综合能力的一类。它要解决的核心问题是给定一批带优先级、到达时间、执行时长的任务按某种调度策略决定执行顺序并统计周转时间、等待时间等指标。适合正在做数据结构课设/毕设的学生也适合想拿一个完整 C/C 工程练手的从业者。这份资源给的就是一套可直接参考的完整实现下面按需求、设计、实现、避坑、验收的顺序拆开讲。2. 需求与选型调度器要支持哪些策略、为什么用 C/C 手写数据结构2.1 功能需求拆解任务模型与调度指标先把“任务”定义清楚不然后面全是糊涂账。一个任务通常包含这些字段任务 ID、名称、优先级数值越小优先级越高或反之团队内必须统一、到达时间、需要执行的时间片、剩余时间、状态就绪/运行/完成。调度器要支持至少三种策略先来先服务FCFS、短作业优先SJF、优先级调度进阶一点再加时间片轮转RR。每种策略跑完要输出一张统计表包含每个任务的开始时间、完成时间、周转时间完成减到达、带权周转时间周转除以执行时长、平均周转时间和平均等待时间。这里有个容易被忽略的点到达时间。很多课设版本默认所有任务在 0 时刻同时到达那 SJF 和优先级调度就退化成一次排序体现不出“调度”二字。合格的负责人一般会要求任务按到达时间动态进入就绪队列调度器在每个时间点从“已到达且未完成”的任务里挑一个执行。这样才需要真正的就绪队列管理也才配得上“数据结构课程设计”的难度。指标计算建议单独抽一个统计模块不要在调度主循环里到处 printf。原因是答辩时老师很可能让你“换一组数据再跑一遍”如果统计逻辑和调度逻辑耦合改一处崩三处。我一般会把结果先写进一个Result结构体数组最后统一格式化输出这样既好调试也方便后面导出成 CSV 做对比。2.2 技术选型为什么坚持手写堆和链表而不是调 STL题目是数据结构课设选型的核心原则是“数据结构要自己实现”。用std::priority_queue确实三行搞定但答辩时老师问“堆的 sift_up 和 sift_down 怎么写的”你答不上来就是硬伤。所以优先队列必须手写二叉堆就绪队列用链队列或循环队列已完成任务用顺序表存起来做统计。C 可以用类和模板但底层容器自己写纯 C 就用结构体加函数指针模拟多态。编译器建议 gcc/g 7 以上或任意主流 IDE 自带的 MinGW标准用 C11 或 C11 即可不要引入第三方库。文件组织上头文件放结构体定义和函数声明源文件按模块拆task.c任务管理、heap.c优先队列、queue.c就绪队列、scheduler.c调度主逻辑、stats.c统计输出、main.c菜单与测试入口。这样拆的好处是每个文件职责单一答辩演示时可以逐个讲也方便你后期加策略时只动 scheduler。提示优先级数值方向一定要在头文件里用注释写死比如“数值越小优先级越高”否则队友或未来的你一定会搞反。3. 总体设计模块划分、核心数据结构与调度主循环3.1 模块职责与数据流整个系统分四层输入层负责读任务支持手动输入和文件批量导入、调度层负责按策略选任务、执行层负责推进时间片并更新任务状态、统计层负责汇总指标。数据流是任务数组 → 按到达时间排序 → 逐个入就绪队列 → 调度器每轮从就绪结构取一个 → 执行并更新剩余时间 → 完成后进结果表。核心数据结构有三样。第一是任务结构体字段前面已列。第二是优先队列二叉堆用数组存任务指针或索引比较函数按优先级或剩余时间切换这样同一套堆代码能服务优先级调度和 SJF。第三是链队列用于 FCFS 和 RR 的就绪管理。已完成任务用动态数组存因为数量已知且只追加。/* task.h —— 任务模型与状态定义 */ typedef enum { READY, RUNNING, DONE } TaskState; typedef struct { int id; /* 任务编号唯一 */ char name[32]; /* 任务名仅用于展示 */ int priority; /* 数值越小优先级越高全项目统一 */ int arrive; /* 到达时间 */ int burst; /* 需要执行的总时间 */ int remain; /* 剩余执行时间 */ int start; /* 首次开始时间-1 表示未开始 */ int finish; /* 完成时间 */ TaskState state; } Task;这段结构体是整个项目的地基。remain字段是为时间片轮转和抢占式调度准备的非抢占式策略里它等于 burst 减去已执行量。start初始化为 -1 很关键统计等待时间时要用“开始时间减到达时间”如果没开始过就说明还在排队不能算错。priority的方向在注释里写死避免后面堆比较函数写反。3.2 调度主循环时间推进与就绪队列维护主循环的骨架是“时间从 0 开始每轮把当前时刻到达的任务塞进就绪结构然后取一个执行”。非抢占式策略一次执行完整个 burst抢占式RR每次执行一个时间片后把任务放回队尾。下面是非抢占式优先级调度的核心逻辑其他策略只需替换“取任务”那一步。/* scheduler.c —— 非抢占式优先级调度主循环 */ void run_priority(Task tasks[], int n, Result out[]) { MinHeap heap; heap_init(heap, n); /* 堆容量等于任务数 */ int t 0, done 0, idx 0; /* 先按到达时间排序保证按序入堆 */ qsort(tasks, n, sizeof(Task), cmp_by_arrive); while (done n) { /* 把所有已到达任务压入优先队列 */ while (idx n tasks[idx].arrive t) { heap_push(heap, tasks[idx]); idx; } if (heap_empty(heap)) { /* 当前无就绪任务时间跳到下一个到达点 */ t tasks[idx].arrive; continue; } Task *cur heap_pop(heap); if (cur-start -1) cur-start t; /* 记录首次开始时间 */ t cur-burst; /* 非抢占一次跑完 */ cur-finish t; cur-state DONE; out[done] make_result(cur); /* 写入统计结果 */ } }这段代码有三个设计点值得讲。第一heap_push的比较函数按priority升序堆顶永远是最该执行的任务如果换成 SJF只改比较函数为按burst升序即可堆本身不动这就是把比较逻辑抽出来的价值。第二空闲时间跳转那一段t tasks[idx].arrive是很多同学漏掉的如果不跳时间会空转统计出的等待时间偏大。第三start只在第一次执行时赋值保证等待时间计算正确。时间片轮转则要把“一次跑完”改成“跑 min(remain, quantum)”没跑完就heap_push回队列RR 用普通队列而非堆。quantum 一般取 2 到 4太小上下文切换开销大太大退化成 FCFS答辩时老师常问这个取值依据你要能说出“在演示数据规模下取 3 能明显体现轮转效果”。3.3 统计输出指标公式与格式化表格统计层只做一件事把Result数组算成平均值并打印。周转时间 finish - arrive带权周转 周转 / burst等待时间 周转 - burst。注意 burst 为 0 的任务要过滤否则除零。输出用固定列宽对齐别用制表符不同终端宽度下会错位。/* stats.c —— 汇总并打印调度指标 */ void print_stats(Result out[], int n) { double sum_turn 0, sum_wturn 0, sum_wait 0; printf(%-6s%-8s%-8s%-8s%-10s%-10s\n, ID, 到达, 开始, 完成, 周转, 带权周转); for (int i 0; i n; i) { int turn out[i].finish - out[i].arrive; int wait turn - out[i].burst; double wt out[i].burst ? (double)turn / out[i].burst : 0.0; sum_turn turn; sum_wait wait; sum_wturn wt; printf(%-6d%-8d%-8d%-8d%-10d%-10.2f\n, out[i].id, out[i].arrive, out[i].start, out[i].finish, turn, wt); } printf(平均周转 %.2f 平均带权周转 %.2f 平均等待 %.2f\n, sum_turn / n, sum_wturn / n, sum_wait / n); }这段代码本身不复杂但它是答辩时最容易被追问的地方。老师可能让你手算一个任务的周转时间对一下输出所以公式必须背下来周转 完成 - 到达等待 周转 - 执行。带权周转用 double 且判断除零是血泪经验——曾经有组数据里混进一个 burst 为 0 的测试任务程序直接崩了答辩现场很尴尬。4. 关键模块实现手写二叉堆、链队列与多策略切换4.1 二叉堆的插入与下沉优先队列的核心堆用数组实现下标从 0 开始父节点(i-1)/2左子2i1右子2i2。插入时放到末尾再上浮弹出时把末尾元素换到堆顶再下沉。比较函数用函数指针传入这样优先级调度和 SJF 共用一套堆代码。/* heap.c —— 手写最小堆比较函数可替换 */ typedef int (*CmpFn)(const Task*, const Task*); static void swap(Task **a, Task **b) { Task *t *a; *a *b; *b t; } static void sift_up(MinHeap *h, int i) { while (i 0) { int p (i - 1) / 2; if (h-cmp(h-data[i], h-data[p]) 0) break; swap(h-data[i], h-data[p]); i p; } } static void sift_down(MinHeap *h, int i) { int n h-size; while (1) { int l 2 * i 1, r l 1, m i; if (l n h-cmp(h-data[l], h-data[m]) 0) m l; if (r n h-cmp(h-data[r], h-data[m]) 0) m r; if (m i) break; swap(h-data[i], h-data[m]); i m; } }sift_up和sift_down是堆的两个命门答辩必问。上浮用于插入从末尾一路和父节点比比父小就换下沉用于弹出堆顶换到末尾后从根一路和较小的子节点比。比较函数用 0和 0的边界要统一否则相等元素的相对顺序会乱虽然调度结果不受影响但调试时输出顺序飘忽会让人怀疑代码有 bug。堆里存的是Task*指针而非结构体拷贝避免大结构体频繁复制也保证调度器改的remain能反映到原数组。4.2 链队列与时间片轮转就绪管理的另一套结构FCFS 和 RR 用链队列节点存任务指针队尾入队、队头出队。RR 的关键是“执行一个时间片后若未完成则重新入队”并且新到达的任务要在当前任务放回之前入队否则顺序会错。/* queue.c —— 链队列供 FCFS 与 RR 使用 */ void run_rr(Task tasks[], int n, int quantum, Result out[]) { Queue q; queue_init(q); int t 0, done 0, idx 0; qsort(tasks, n, sizeof(Task), cmp_by_arrive); while (done n) { while (idx n tasks[idx].arrive t) queue_push(q, tasks[idx]); if (queue_empty(q)) { t tasks[idx].arrive; continue; } Task *cur queue_pop(q); if (cur-start -1) cur-start t; int run cur-remain quantum ? cur-remain : quantum; t run; cur-remain - run; /* 先让新到达任务入队再把当前任务放回队尾 */ while (idx n tasks[idx].arrive t) queue_push(q, tasks[idx]); if (cur-remain 0) queue_push(q, cur); else { cur-finish t; cur-state DONE; out[done] make_result(cur); } } }这段代码里“先入新到达、再放回当前”的顺序是 RR 正确性的关键。如果反过来一个刚到达的高优先级任务会被排到当前任务后面轮转顺序就错了。quantum 参数从命令行或菜单传入方便答辩时现场改值演示不同效果。队列节点记得在出队后 free虽然课设数据量小泄漏不明显但用 valgrind 一跑就露馅养成习惯没坏处。4.3 策略切换与菜单驱动一个入口跑四种算法主函数用菜单驱动用户选策略、选数据来源手动/文件、设时间片然后调对应函数。策略用枚举加 switch 分发避免 if-else 堆成山。文件格式建议每行“id name priority arrive burst”用fscanf读读完校验数量。/* main.c —— 菜单与策略分发 */ int main(void) { Task tasks[MAX_TASK]; Result out[MAX_TASK]; int n load_tasks(tasks.txt, tasks, MAX_TASK); /* 返回任务数 */ int choice, quantum 3; printf(1.FCFS 2.SJF 3.优先级 4.时间片轮转\n请选择: ); scanf(%d, choice); switch (choice) { case 1: run_fcfs(tasks, n, out); break; case 2: run_sjf(tasks, n, out); break; case 3: run_priority(tasks, n, out); break; case 4: run_rr(tasks, n, quantum, out); break; default: printf(无效选择\n); return 1; } print_stats(out, n); return 0; }菜单驱动的好处是答辩演示流畅老师点什么你跑什么。load_tasks返回实际读到的任务数防止文件行数超过数组容量。每种策略跑之前要把任务数组重置remain 恢复成 burst、start 恢复 -1否则连续跑两种策略时第二种会拿到被改过的状态这是最常见的翻车点之一后面避坑章节还会展开。5. 避坑与排查五处最容易在答辩现场翻车的地方5.1 优先级方向搞反结果全错但程序不报错现象优先级调度跑出来的顺序和预期完全相反但程序不崩溃、不报错。原因堆比较函数写成了“数值大优先”而任务数据是按“数值小优先”录入的两处约定不一致。解决在task.h里用注释和宏统一方向比较函数只写一处所有策略复用测试时故意放两个优先级 1 和 9 的任务看谁先跑一眼就能验证。5.2 连续跑多种策略时状态未重置现象先跑 FCFS 再跑 SJF第二次结果明显不对周转时间偏小或任务显示已完成。原因第一次调度把remain减到 0、start和finish都写了值第二次直接拿脏数据跑。解决抽一个reset_tasks函数每次调度前把remainburst、start-1、finish0、stateREADY全部恢复在菜单分发处统一调用。5.3 空闲时间未跳转导致等待时间虚高现象任务到达时间有间隔时平均等待时间比手算大很多。原因主循环里当前无就绪任务时t空转把空闲时间也算进了某些任务的等待。解决无就绪任务时直接t 下一个未到达任务的 arrive跳过空档。验证方法构造一个 0 时刻到达、一个 10 时刻到达的两任务数据手算等待时间对输出。5.4 堆数组越界或弹出空堆现象任务数较多时程序崩溃或输出乱码。原因堆容量按任务数初始化但入堆逻辑在边界时刻可能重复压入同一任务或者heap_pop前没判空。解决入堆前检查stateREADY且未被压过用一个pushed标记位heap_pop前必须heap_empty判断空堆时走时间跳转分支。5.5 文件读取不校验脏数据直接进调度现象换一组测试文件就崩或出现 burst 为负的任务。原因fscanf返回值没检查读到格式不对的行仍然把垃圾值写进结构体。解决逐行读检查fscanf返回 5 个字段才接收burst 和 arrive 做非负校验非法行打印行号跳过。答辩时老师很可能拿一份“脏数据”试你的健壮性这一步做了就是加分项。6. 验收与答辩怎么用一组数据证明你的调度器是对的验收的核心不是“程序能跑”而是“结果可复现、可手算核对”。我一般会准备三组数据第一组 5 个任务、到达时间全为 0用来验证 SJF 和优先级排序是否正确手算简单第二组 5 个任务、到达时间错开用来验证空闲跳转和动态入队第三组 8 个以上任务、burst 有长有短用来演示 RR 的轮转效果和平均指标对比。每组数据都提前手算好平均周转时间答辩时当场跑、当场对对上了老师就没话说。下面这张对照表建议直接放进答辩 PPT四种策略跑同一组数据指标差异一目了然也顺便证明你的多策略切换是真的在工作不是四份代码拼的。策略平均周转平均带权周转平均等待适用场景说明FCFS偏大偏大偏大实现最简单长任务拖累短任务SJF最小最小最小理论最优但需预知执行时间优先级中等中等中等体现业务权重可能饿死低优先级RR中等偏大中等中等响应公平时间片取值影响大答辩陈述按“需求—结构—算法—验证”四段走每段控制在两分钟。需求讲任务模型和三种以上策略结构讲模块划分和三个核心数据结构算法重点讲堆的 sift_up/sift_down 和 RR 的入队顺序这是区分“自己写的”和“抄的”的关键验证就拿上面三组数据现场跑。老师追问时不要慌大概率问“为什么用堆不用有序数组”——答“插入 O(log n) 对比有序数组插入 O(n)任务动态到达时堆更合适”一句话到位。还有一个具体技巧把make_result和print_stats的输出版本和调试版本分开调试版多打印每个时间点的就绪队列内容答辩前用调试版跑一遍确认流程答辩时切正式版输出干净表格。这个习惯让我少挨了很多“你这中间过程怎么跳了”的追问。从那以后我每次交课设前都强制走一遍“手算一组、跑一组、对一组”的流程再没在指标上翻过车。希望帮到你。本文还有配套的精品资源点击获取