9. 线程调度

本章代码对应 commit :65ccec7b180cd56e645f359343a793e75a49eb98

概要

多线程并发执行需要调度器的辅助。调度器的作用是在合适的时刻选择线程执行,并在合适的时候切换线程,防止一个线程占用或多的资源或阻塞,从而实现 cpu 资源分配相对“公平”。本章我们将:

  1. 假想我们已经有了一个调度算法。

    算法和数据结构是分离的,所以在实现调度器的时候不依赖于算法

  2. 创建线程池(thread pool)用于保存所有线程。

  3. 创建线程管理器(processor)通过调度算法管理 thread pool。

  4. 实现线程调度相关函数。

  5. 引入 Round Robin 调度算法。

调度算法

//in process/scheduler.rs
impl Scheduler {
    pub fn new(max_time_slice: usize) -> Scheduler { /* TODO */ }

    pub fn push(&mut self, tid: Tid) { /* TODO */ }

    pub fn pop(&mut self) -> Option<Tid> { /* TODO */ }

    pub fn tick(&mut self) -> bool { /* TODO */ }

    pub fn exit(&mut self, tid: Tid) { /* TODO */ }
}

这是本章我们使用的调度算法的对外接口。功能包括创建(new)、添加线程(push)、获取即将被调用的线程(pop)、提醒算法时钟周期的到来(tick)和退出线程时将线程从调度算法中移出(exit)。

线程池(ThreadPool)

线程的运行状态包括但不限于:等待运行、运行中、睡眠和等待退出。这里我们创建一个枚举类型作为进程的状态类型:

Tid 是线程 id 。就像每个人的身份证号都是不一样的,每一个线程都有独一无二的 id ,这时线程的标识。

线程池是用于存放线程的容器,只需要包含线程的信息和线程调度算法。创建 process/thread_pool.rs

Box 允许我们将一个值放在堆上而不是栈上,留在栈上的则是指向堆数据的指针。除了数据被储存在堆上而不是栈上之外,Box 没有额外的性能损失,不过也没有额外的功能。

让我们先来实现他的构造函数和向线程池中加入线程的功能:

构造函数规定了线程的最大数量 size 和调度算法。由于线程数组是已经创建好的,但是默认内容为 None ,所以在添加线程的时候只需要将从 Vec 中找到一个未使用的位置,把新线程的信息传递过去就可以了。同时,不要忘记为调度算法传入线程 id 。

调度器

由于创建的调度器是全局的,需要考虑一些安全问题和异步问题。为此需要对成员进行一些包装,不过这不影响实现思路。创建 processor.rs

一个实现了 Sync trait 的类型可以安全的在多个线程中拥有其值的引用。因为他是标记 trait ,所以不需要手动实现。 UnsafeCell 内的元素不严格区分 immutable 和 mutable 。

调度器接口实现

这里先列出功能简单明了的几个函数:

在进行线程切换时,为了防止因为中断引起线程切换出错,需要关闭中断,之后再恢复到原先的中断状态。这里我们先实现三个与中断控制相关的函数:

接下来开始实现三个与调度直接相关的重要函数。

其中调用了一些 ThreadPool 中尚未实现的函数,将于之后实现

run

这是整个调度过程最核心的函数,由 idle 线程调用。具体实现如下:

tick

每产生一次时钟中断(即经过一个时钟周期),就需要通知线程池,让他通过调度算法判断是否需要切换线程:

inner.pool.tick 会通知线程池和调度算法已经过了一个时钟周期,同时返回一个布尔值:是否需要进行线程切换。如果需要切换至其他线程,则先切换至 idle 线程,然后由 idle 进行调度(回到 Processer.run)。

exit

当线程任务完成之后,就可以通过 Processor.exit 结束自己(结束当前线程):

线程池接口实现

线程池接口的功能在前文已经提及,也可由函数名判断函数功能。具体实现也较为简单,所以直接给出实现:

这里用到了 Option.take ,功能与所有权转移或浅拷贝相似

引入 Round Robin 调度算法

在 Cargo.toml 中加入:

创建 process/scheduler.rs

最后,由于 Processor.add_thread 需要 Box<Thread> 类型的参数,所以我们修改一下 struct Thread 的构造函数:

至此我们的调度器已经全部完成,让我们来测试一下他吧:

FIX runtime error: panicked at 'Processor is not initialized'

执行 make run ,发现kernel出现了运行时错误:

从字面意思上看,是Processor这个结构没有初始化!但仔细检查代码,在如下代码中有new和记忆棒初始化的过程:

所以,不应该是这部分的问题。再进一步检查,在init.rs中的rust_main函数在调用process_init()之前,有一些unsafe代码,怀疑是它造成的???。试着把代码删除。再执行 make run ,发现我们的调度器已经能够自动切换线程,线程来回切换也可以正常恢复原先的工作环境,并且在线程结束后能够正常结束退出。

Last updated

Was this helpful?