1. 单核CPU任务调度问题解析
最近在准备华为OD机考时遇到一个非常典型的操作系统调度问题——单核CPU任务调度。这个问题不仅考察了对优先权调度算法的理解,也考验了编程实现能力。作为一个经历过多次机考的老手,我想分享一下这个问题的完整解决思路和Java实现方案。
这个问题模拟了操作系统中的任务调度场景:我们有一个单核CPU和多个需要处理的任务,每个任务有ID、优先级、执行时间和到达时间四个属性。CPU在任何时刻只能运行一个任务,我们需要按照"可抢占优先权调度"算法来安排任务的执行顺序。
2. 可抢占优先权调度算法详解
2.1 算法核心规则
可抢占优先权调度(Preemptive Priority Scheduling)是操作系统中常见的一种调度策略,其核心规则可以总结为:
- 高优先级抢占:当一个更高优先级的任务到达时,CPU必须立即暂停当前任务(无论是否完成)去执行这个新任务
- 低优先级等待:如果新到达的任务优先级不高于当前运行任务,则新任务需要等待
- 空闲时选择策略:当CPU空闲且有等待任务时,选择优先级最高的任务执行;优先级相同时选择到达时间最早的任务
2.2 算法执行流程
在实际实现中,这个算法的执行流程可以分解为以下步骤:
- 初始化任务队列,按到达时间排序
- 维护一个就绪队列(等待执行的任务)
- 维护当前运行任务和其剩余执行时间
- 模拟时间推进,处理任务到达和完成事件
- 在每次事件发生时重新评估当前运行任务
3. Java实现方案
3.1 数据结构设计
首先我们需要设计合适的数据结构来表示任务和调度状态:
java复制class Task {
int id; // 任务ID
int priority; // 优先级(数字越大优先级越高)
int executeTime;// 执行时间
int arriveTime; // 到达时间
// 构造函数
public Task(int id, int priority, int executeTime, int arriveTime) {
this.id = id;
this.priority = priority;
this.executeTime = executeTime;
this.arriveTime = arriveTime;
}
}
3.2 核心算法实现
实现的核心在于正确处理任务调度的时间推进和状态转换:
java复制public class CPUScheduler {
public static void scheduleTasks(List<Task> tasks) {
// 按到达时间排序
tasks.sort(Comparator.comparingInt(t -> t.arriveTime));
// 优先队列:按优先级降序,同优先级按到达时间升序
PriorityQueue<Task> readyQueue = new PriorityQueue<>(
(t1, t2) -> t1.priority != t2.priority ?
t2.priority - t1.priority :
t1.arriveTime - t2.arriveTime
);
int currentTime = 0;
int taskIndex = 0;
Task currentTask = null;
int remainingTime = 0;
while (taskIndex < tasks.size() || !readyQueue.isEmpty() || currentTask != null) {
// 处理新到达的任务
while (taskIndex < tasks.size() && tasks.get(taskIndex).arriveTime <= currentTime) {
Task newTask = tasks.get(taskIndex++);
readyQueue.offer(newTask);
// 检查是否需要抢占
if (currentTask != null && newTask.priority > currentTask.priority) {
readyQueue.offer(currentTask);
currentTask = newTask;
remainingTime = newTask.executeTime;
readyQueue.poll(); // 移除刚加入的newTask
}
}
// 选择下一个任务执行
if (currentTask == null && !readyQueue.isEmpty()) {
currentTask = readyQueue.poll();
remainingTime = currentTask.executeTime;
}
// 执行任务(推进时间)
if (currentTask != null) {
int timeElapsed = 1;
remainingTime -= timeElapsed;
if (remainingTime == 0) {
System.out.println(currentTime + 1 + " " + currentTask.id);
currentTask = null;
}
}
currentTime++;
}
}
}
3.3 输入处理与主函数
完整的解决方案还需要处理输入输出:
java复制public static void main(String[] args) {
Scanner scanner = new Scanner(System.in);
List<Task> tasks = new ArrayList<>();
while (scanner.hasNextLine()) {
String line = scanner.nextLine().trim();
if (line.isEmpty()) break;
String[] parts = line.split(" ");
int id = Integer.parseInt(parts[0]);
int priority = Integer.parseInt(parts[1]);
int executeTime = Integer.parseInt(parts[2]);
int arriveTime = Integer.parseInt(parts[3]);
tasks.add(new Task(id, priority, executeTime, arriveTime));
}
scheduleTasks(tasks);
}
4. 算法复杂度分析
4.1 时间复杂度
- 初始排序:O(n log n)
- 每个任务进出优先队列:O(log n)
- 总体时间复杂度:O(n log n)
4.2 空间复杂度
- 存储任务列表:O(n)
- 优先队列:最坏情况下O(n)
- 总体空间复杂度:O(n)
5. 测试用例与验证
5.1 基础测试用例
code复制1 3 5 0
2 1 3 1
3 2 4 2
预期输出:
code复制5 1
9 3
13 2
5.2 抢占测试用例
code复制1 2 5 0
2 3 2 2
3 1 3 4
预期输出:
code复制2 1
4 2
7 1
10 3
5.3 边界测试用例
code复制1 1 1 0
预期输出:
code复制1 1
6. 常见问题与调试技巧
6.1 时间推进问题
在模拟时间推进时,容易犯的错误是时间步长设置不合理。建议:
- 使用最小时间单位(通常是1)推进
- 在每个时间点检查是否有新任务到达
- 正确处理任务完成和抢占的边界条件
6.2 优先队列比较器
优先队列的比较器是实现的关键,常见错误包括:
- 优先级比较方向错误(记住数字越大优先级越高)
- 相同优先级时未正确处理到达时间顺序
- 比较逻辑不一致导致队列排序异常
6.3 抢占逻辑实现
实现抢占时需要注意:
- 正确处理当前任务的保存和恢复
- 避免重复添加任务到队列
- 确保抢占后剩余执行时间正确更新
7. 性能优化建议
对于大规模任务调度,可以考虑以下优化:
- 事件驱动:改为基于事件的调度,只在任务到达或完成时推进时间
- 批处理:对于同时到达的任务进行批处理
- 高效数据结构:考虑使用更高效的数据结构管理就绪队列
8. 实际应用场景扩展
虽然这是一个简化的问题,但其核心思想在实际系统中有广泛应用:
- 操作系统进程调度
- 实时系统任务管理
- 网络数据包优先级处理
- 游戏引擎中的事件处理
理解这个基础算法后,可以进一步学习更复杂的调度策略,如:
- 多级反馈队列调度
- 轮转调度
- 最早截止时间优先调度
在实现这个算法的过程中,我最大的收获是对操作系统调度机制有了更直观的理解。特别是在处理抢占逻辑时,需要考虑各种边界情况,这让我意识到系统设计中的细节重要性。建议在练习时多构造一些极端测试用例,比如同时到达的任务、连续抢占等情况,这能帮助发现实现中的潜在问题。