线程的调度算法


一、前言

在业务应用开发中,若涉及线程相关,一般会将线程数目设置为CPU的逻辑核数以便最大限度利用CPU。操作系统提供了通用的调度能力,但是在某些特殊场景中需要特殊处理。例如,在数据库系统中,为保证高优先级线程得到快速处理(比如,事务操作)、避免被其他线程中断(分片时间到)或者被迁移到其他CPU核(导致CPU cache失效),会将相关线程与部分CPU核绑定,只留剩余的CPU核参与通用调度分配。

二、调度算法

操作系统内核调度的对象的确是线程,但是在讲解调度算法的时候,若仍然用“线程”,在某些场景会出现一些字面意思的冲突(和线程实际的执行行为),并且调度算法具有普适性,故这里做一层抽象,改用“任务(Job)”来阐述调度算法。

在资源一定的情况下,调度算法需要在吞吐量(Throughput)、平均响应时间(延迟,Average Response Time)、公平性、调度引起的额外开销(Overhead)等几个方面做权衡。

1.先进先出算法(FIFO,First-In-First-Out)

按照任务进入队列的顺序,依次调用,执行完一个任务再执行下一个任务,只有当任务结束后才会发生切换。

  • 优点:

最少的任务切换开销(因为没有在任务执行过程中发生切换,故任务切换开销为0)
最大的吞吐量(因没有任务切换开销,在其他一定的情况下,吞吐量肯定是最大的)
最朴实的公平性(先来先做)

  • 缺点:

平均响应时间高:耗时只需10毫秒的任务若恰巧在耗时1000毫秒的任务后到来,他则需要1010毫秒才能执行完成,绝大部分时间都花在等待被调度了。

  • 适用场景:

队列中任务的耗时差不多的场景。

2.最短耗时任务优先算法(Shortest Job First, SJF)

按照任务的耗时长短进行调度,优先调度耗时短的任务,这个算法有个前提,需要预先知道每个任务的耗时情况,这在实际情况中是不大现实的。另外,这个时间是指任务剩余还需要的执行时间,举例,一个耗时1小时的任务还剩10秒执行完成,这个时候若再来一个耗时1分钟的任务,调度仍然还是继续执行完那个耗时1小时的任务,因为他剩余的时间是10秒,比1分钟短,所以此算法又叫最短剩余时间任务有限算法(SRTJ),能够解决FIFO算法中短耗时任务等待前面耗时长任务的窘境。

  • 优点:

平均响应时间较低:这里有一点,因为将时间长的任务无限往后推移,实际计算的平均响应时间的任务都是执行较快的任务,统计出来的平均响应时间必然较低的。

  • 缺点:

耗时长的任务迟迟得不到调度,不公平,容易形成饥饿
频繁的任务切换,调度的额外开销大

  • 适用场景:

几乎无适用场景。

3.时间片轮转算法(Round Robin)

给队列中的每个任务一个时间片,第一个任务先执行,时间片到了之后,将此任务放到队列尾部,切换到下个任务执行,这样能解决SJF中耗时长任务饥饿的问题。算法介于FIFO和SJF之间,若时间片足够大,则退化到FIFO,若分片足够小(假设不考虑任务切换的开销),则任务的完成时间顺序是以耗时从小到大排列。

  • 优点:

每个任务都能够得到公平的调度
耗时短的任务即使落在耗时长的任务后面,也能够较快的得到调度执行

  • 缺点:

任务切换引起的调度开销较大,需要多次切换任务上下文(特别是CPU的Cache,多次切换容易导致Cache完全不命中,需要重新从内存加载,这个非常耗时)
时间片不好设置(若设置短了,调度开销大,若设置长了,极端情况是退化到FIFO)

  • 适用场景:

队列中是耗时差不多的任务(比如,多路视频流处理)

  • 不适用场景:

计算型任务和I/O型任务混合的队列

4.最大最小公平算法( Max-Min Fairness )

顾名思义,此算法是为了保证公平性,最初源自通信网络的带宽分配与控制场景。基本思路是先平均分配,若多的话,再将剩余的给其他人再次平均分配,若不够的话,需要等待。

场景举例,某一资源容量为10,现有4个使用方需要使用(A、B、C、D),要求的资源数分别是2、2.6、4、5,需要经过多轮计算:

  • 第一轮分配,4个使用方(A、B、C、D)参与分配,每个平均分得2.5个资源,A只需要使用2,还剩0.5, B、C、D在本轮都不够用,只好被推迟。
  • 第二轮分配,3个使用方(B、C、D)参与分配,分别可获得2.5 + 0.5/3 = 2.6666, B只需要要2.6,则还剩0.06666,可给剩余2个分配,C、D在本来都不够用,只好被推迟。
  • 第三轮分配,2个使用方(C、D)参与分配,分别获得2.5+ 0.5/3 + 0.06666/2 = 2.699999 , C和D分别获得2.6999。(后面若还有新的资源,则可以分配给C和D,以便能满足他们的诉求)

为了体现重要性,在此基础上引入了带权重的最大最小公平算法。

场景举例,某一资源容量为16,给A、B、C、D分别设置权重为5、8、1、2(权重的最小粒度为1,数值越大,权重越高),要求的资源数量分别是4、2、10、4,需要经过多轮计算:

  • 第一轮分配,分配给A、B、C、D的资源分别是5、8、1、2, A剩余1个资源,B剩余6个资源,C、D因资源不够用,处理延期。
  • 第二轮分配,在A和B剩余的总共7个资源中,根据C、D的权重,分别获得71/3、72/3 个,加上第一轮分配的数量,C、D分别拥有3.3333、6.6666,C仍然不能满足,D能够满足,D用完之后还剩2.6666,这个可以给C用。
  • 第三轮分配,C目前拥有6,仍然不够,处理延期 (后面若还有新的资源,则可以分配给C,以便能满足他的诉求)。

5.Multi-level Feedback Queue(MFQ)

此算法兼顾响应时间( Responsiveness )、低调度开销(Schedule overload)、饥饿避免(Starvation-Freedom)、公平(Fairness),在Windows,MacOS,Linux内核调度系统中都有使用。算法内容:

  • 有多个Level,从上到下,优先级越来越低,分片时长越来越大。
  • 位于高优先级Level的任务可以抢占低优先级Level的任务。
  • 新任务首先位于高优先级Level,当一个时间片用完之后,若任务结束,则正常退出系统,若任务还没有结束,则下滑到低一等级的Level。若是因等待I/O而主动让出CPU处理,则停留在当前Level(或者提高一个Level)。
  • 同一Level的任务采用Round Robin算法。
  • 为避免系统中有太多的I/O任务而导致计算型任务迟迟得不到处理,MFQ算法会监控每个任务的处理耗时、确保其拥有公平的资源分配(按照最大最小公平算法)。在每个Level的所有任务,若有任务还没有用完分配给他的资源,则相应提高他的优先级,反之则降低其优先级。

在这里插入图片描述

6.多CPU核场景下MFQ算法的考虑

在多CPU核的场景下,若共用一个MFQ,则会出现:

  • 随着CPU核不断增长,对MFQ锁的争抢会越来越严重。
  • 因MFQ的最新状态都是存在上次执行的CPU核的Cache,当前CPU核需要从远端CPU核的Cache去拉取最新数据、然后存到Local Cache,这个行为会很耗时。

为解决上述问题,会为每个CPU核分配一个MFQ,同时采用Affinity Scheduling策略,保证同一线程的执行尽量落在同一CPU核(挂起中断后重新执行)。

若出现某些CPU核特别繁忙,会进行Rebalancing操作,将部分线程迁移到其他空闲的CPU核执行。线程在同一个CPU核处理可以最大化利用CPU Cache,避免频繁的从内存加载数据到CPU Cache。Golang语言处理多个协程并发的时候,也会尽量保证相关的协程由同一个底层线程进行处理,也是基于重用CPU Cache的因素。

三、操作系统内核层面的考量

进程是操作系统进行”资源分配“的基本单位,线程是操作系统进行“调度”的基本单位,从这句话的字面解读,似乎进程和“调度”没有太多关系,但是实际上,内核在进行线程调度的时候会去参照线程是否属于同一个进程(当其他条件都一样的情况下)。有两个策略会将进程因素考虑进去:

  • Gang Scheduling:尽量将同一进程中的线程同时调度,而不是随机从多个进程挑选内核数目的线程进行调度。

  • Space Sharing:将CPU核进行划分,若同时有多个进程运行,只让某个进程占用部分CPU核来进行并发,而不是在某一时间分片内所有CPU核被同一个进程的线程占满。


以上内容来源于网络知识总结,如有侵权请私信联系立即删除:)

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
Java中的线程调度算法主要依赖于操作系统线程调度算法。Java虚拟机(JVM)会为每个Java线程创建一个对应的操作系统线程,并通过操作系统线程调度器来进行调度。 常见的线程调度算法包括: 1. 时间片轮转调度算法:每个线程被分配一个固定大小的时间片,当时间片用完后,调度器会将该线程挂起,继续执行下一个线程。这种算法可以保证每个线程都有公平的执行机会。 2. 优先级调度算法:每个线程被赋予一个优先级,优先级高的线程会被优先执行。这种算法可以根据线程的重要性和紧急性进行调度,但可能存在优先级反转和饥饿问题。 3. 先来先服务调度算法:按照线程请求的先后顺序进行调度,先到达的线程先执行。这种算法简单直观,但可能导致长时间任务阻塞后面的短时间任务。 4. 多级队列调度算法:将线程划分为多个优先级队列,每个队列采用不同的调度算法。例如,可以将高优先级任务放在一个队列中,低优先级任务放在另一个队列中。这种算法可以根据任务的优先级和类型进行灵活的调度。 需要注意的是,Java中的线程调度算法受到操作系统的限制和影响,不同的操作系统可能具有不同的线程调度策略。此外,Java也提供了一些线程调度相关的API,如Thread类的yield()方法和sleep()方法,可以在一定程度上影响线程的调度行为。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值