线程是如何调度的?

在操作系统中,线程是如何调度的?或者是线程调度有哪些方法?

最近在补充一些操作系统的知识,线程调度是操作系统无法回避的问题。对其由浅入深的解决思路大有感触,在此记录。

先到先处理

对于操作系统而言,线程相当于一个个待执行的任务。最常见用于任务调度的是队列,队列是任务调度的最简单模型,遵从先到先处理的原则,一个任务处理完成之后,才会处理下一个任务。

相对而言,队列模型是最公平的,任务的执行顺序只与进入队列的时间相关,同时,由于不存在任务切换等,所以没有额外的逻辑代码开销。

下面是用js实现的一个任务调度的队列模型:

// 模拟sleep
function sleep(ms) {
    for (let t = Date.now(); Date.now() - t <= ms;);
}

// 调度器对象
class scheduler {
    constructor() {
        // 队列
        this.queue = []
    }

    // 压入任务
    pushTask(task) {
        this.queue.push(task)
    }

    // 获取下一个待执行的任务
    next() {
        if (this.queue.length > 0) {
            // 获取最先进入的一个
            return this.queue.shift()
        }
        return undefined
    }

    // 执行
    excute() {
        // 不断的循环执行
        while (true) {
            const nextTask = this.next()
            if (nextTask) {
                nextTask()
            } else {
                sleep(1000)
            }
        }
    }
}

队列模型对于调度器而言是公平的,调度器平等的处理每一个任务,但是对于任务来说是不公平的,由于后进入队列的任务需要等待前面的任务执行完成才能执行。对于一个需要执行1天的任务,让其等待10分钟是可以接受的,但是如果一个需要执行10分钟的任务,需要等待一天,就无法接受,对于这种情况需要考虑短任务优先

短任务优先

对于操作系统,线程可以看作是一个个用户,用户满意度可以衡量调度系统的好坏。我们可以用平均等待时间来近似衡量用户满意度,平均等待时间和满意度成反比,平均等待时间越长,用户满意度越差。

比如一个包含三个任务的队列[10, 2, 5],队列中的每一项代表任务执行时间。

当任务依次执行时,那么平均等待时间为:(0 + 10 + 2) / 3 = 4。

当队列按照任务执行时间由小到大排列后,即队列变成[2, 5, 10],那么平均等待时间为: (0 + 2 + 5) / 3 = 2.33。

从上面两种情况的对比中可以看出,排序后的队列平均等待时间较短,即用户的满意度较高,也就是短任务优先。

调整前文说的调度器对象,在每次获取下一个可执行任务之前,将所有任务按照执行时间由小到大排序:

    // 获取下一个待执行的任务
    next() {
        if (this.queue.length > 0) {
            // 排序
            this.queue.sort((a, b) => a.excuteTime - b.excuteTime)
            // 获取最先进入的一个
            return this.queue.shift()
        }
        return undefined
    }

在队列模型中加入了短任务优先之后,满足了线程调度满意度,但是该模型还存在问题,即如果有重要任务需要插队应该怎么办?解决此问题就需要用到优先级。

优先级

针对任务插队的情况,可以在原有调度对象上增加优先级,即将其内部的一个队列拆分成多个具有不同优先级的队列,调度器总是先获取高优先级中的任务,当高优先级中不存在任务的时候,才去获取低优先级中的任务。

修改前文的调度器对象:

// 调度器对象
class scheduler {
    constructor() {
        // 队列
        this.queue = {
            high: [],
            medium: [],
            low: []
        }
    }

    // 压入任务
    pushTask(task, priority) {
        this.queue[priority].push(task)
    }

    // 获取下一个待执行的任务: 按照优先级依次由高到低取
    next() {
        function getQueueNext(queue) {
            if (queue.length > 0) {
                // 排序
                queue.sort((a, b) => a.excuteTime - b.excuteTime)
                // 获取最先进入的一个
                return queue.shift()
            }
            return undefined
        }

        const priorityMap = Object.keys(this.queue)
        let next
        // 循环执行,当高优先级中没有取到任务,那么就从低优先级中获取
        while (!undefined && priorityMap.length > 0) {
            const currentPriority = priorityMap.shift()
            next = getQueueNext(this.queue[currentPriority])
        }
        return next
    }

    // 执行
    excute() {
        // 不断的循环执行
        while (true) {
            const nextTask = this.next()
            if (nextTask) {
                nextTask()
            } else {
                sleep(1000)
            }
        }
    }
}

当加入优先级之后,当新的任务需要插队执行的时候,可以将其添加到优先级较高的队列中,这样可以保证在当前任务执行完成后,插队的任务可以被优先执行。

虽然加入优先级之后,可以应对需要插队的情况。但是由于目前调度器都是执行完上一次任务之后才执行下一次任务,当出现耗时较长的任务执行到一半的时候,进来一个耗时较短的任务需要执行的情况时,便无法处理。针对这种情况,可以加入抢占机制。

抢占

抢占就是将操作系统的执行能力分时,分成多个执行片段,默认任务只执行一个时间片段,当一个片段内任务被执行完成,就会执行下一个任务,如果未执行完成,也会执行下一个任务,但是当前未执行完成的任务会被终端,重新进入队列排队,

加入抢占功能之后,操作系统的线程执行就变成了一段段的时间片段,每一次执行片段结束,调度系统就会为操作系统分配下一个执行任务。这样就保证不会因为长耗时任务的执行导致短耗时任务无法及时执行。

通过为队列加入优先级和抢占机制之后,当前线程的调度系统已经比较完善成熟。但是在短任务优先一节中提到需要将任务按照耗时长短进行排序,而操作系统是无法预知线程的执行时间的?

多级队列

为了解决操作系统无法预知线程执行时间的问题,需要优化优先级章节中提到的多优先级队列。

假设我们的调度系统中有多级队列,最高优先级的队列用于执行紧急任务,其内部不包含抢占机制,其余的队列中包含抢占机制,并且,优先级越低的队列执行时间片段越长。

如一个调度系统中,包含三种优先级队列,从高到低一次为A,B,C,其中A队列为紧急任务队列,不分时执行,B,C队列为分时队列,B的分时为1s,C的分时为2s。

当紧急任务需要被调度执行的时候,其会被调度系统放入A队列优先执行。

当一个普通任务被调度执行的时候,其会被放入普通的B队列,由于B队列是分时执行的,当1s的时间内,该任务没有被执行完成,那么该任务的执行会被中断,同时调度系统会将该任务放入下一级队列中(C)排队。

当队列的级数越来越多时,可以通过层层筛选将长耗时的任务筛选出来,近似实现短任务优先。

你可能感兴趣的:(操作系统,线程)