goroutine 相关知识6

调度本质上就是一个资源分配算法

  • 调度的基础策略,常见模型,以及 Go 和 Erlang 的一些调度特性

调度机制 抢占 vs 协作

非抢占/协作式调度

一旦将调度资源(如 CPU)分配给某任务后,任务一直执行直到

  • 该任务完成
  • 阻塞
  • 主动让出CPU控制权

实现简单,对共享资源的访问也更安全(如允许使用不可重入函数)

常见算法如 FIFO,STCF(Short time to complete first)等

抢占方

允许调度程序根据某种规则,剥夺当前进程的调度资源,将其分配给其它进程

常见抢占策略

  • 基于时间片: 均分CPU 的算法,又叫轮转调度算法
  • 基于优先级: 给每个进程分配一个优先级,一旦出现一个优先级更高的进程,则停止当前进程并切换到该高优先级进程,这种调度算法又叫优先级抢占
  • 轮转调度和优先级抢占结合: 即相同优先级的进程使用轮转调度,如果遇到更高优先级的进程,则可抢占CPU。现代 OS 如 Linux 通常都使用这种混合调度策略

基于优先级抢占容易出现优先级反转的问题: 优先级低的任务持有一个被优先级高的任务所需要的共享资源,这种情况下,优先级低的任务有资源而得不到CPU,优先级高的资源有CPU而得不到资源,从而阻塞(导致其它中优先级的任务获得执行)或者忙等(可能永远无法获得资源)

解决优先级反转的方案:

  • 给临界区一个高优先级,所有进入该临界区的任务将获得该高优先级,避免其被随意抢占
  • 高优先级任务在等待低优先级进程持有资源时,低优先级进程将暂时获得高优先级进程的优先级,VxWorks采用的方式
  • 禁止中断,也就是在临界区不可被抢占,Linux 采用的就是这种方式,在 thread_info.preeempt_count 记录每个进程当前持锁计数

另外,调度器是不能在进程指令流的任意一点执行打断的,因为进程可能此时正在做任何事情,如系统调用,死循环,锁操作等,要实现任意状态的可抢占性代价是很大的,需要 OS 和 App 的通力配合,特别是在涉及在内核态的时候,目前所有OS都不能在进程执行的任意点进行抢占。只是说不断让抢占区更大,抢占点尽可能地密集

实时 vs 非实时

软实时和硬实时

  • 硬实指系统要有确定的最坏情况下的服务时间,即对于事件的响应时间截止期限无论如何都必须得到满足,通常应用在军工航天领域

  • 软实时只提供统计意义上的实时,比如应用要求在95%的情况下都会确保在规定的时间内执行某个任务,而不一定要求100%

实时系统通常采用抢占调度,实现一个硬实时系统的代价是很高的,要做到进程可以在任意时刻被抢占,在现代OS上来讲,基本是不可能的,因为OS的很多系统调用都是不可被打断的,并且很多操作具备时间的随机性,比如 CPU Cache Miss,Page Fault,CPU Branch Predictor 等等

调度结构

single scheduler

  • 只有一个调度器和一个任务队列
  • 由于不需要锁,所以单核吞吐量很高
  • 主要的缺点在于无法充分利用调度资源(如多核),并且容易出现任务饥饿(多调度器本质也会有这种情况,只不过被掩盖了一些)

multi scheduler with global queue

最大化多核收益,多个调度器,理想情况下是调度器的数量和CPU核心数一致

多个调度器共享一个全局任务队列,Erlang OTP R11B

主要的瓶颈在于全局任务队列的操作需要加锁,并且CPU核心数越多,调度瓶颈越明显,从而限制了调度算法在多核下的扩展性

multi scheduler with local queue

优化全局任务队列的瓶颈问题,借鉴”Cache思维”,给每个调度器分配一个本地的任务队列:

调度器可以无锁操作本地任务队列,显著减少锁竞争,提升多核下的调度效率

引入新问题: 如何尽可能地让各个调度器都随时有事情做(任务分配尽可能均衡)

任务迁移(Task Migration)或任务窃取(Task Stealing)。Erlang R13B+ 和 Go 调度都是基于此结构,但有一些区别

以上三种模型是大多数调度器历经的三个阶段,最终演化得到的是一个多层次,局部性的结构(类似 CPU Cache层级)

erlang/go 调度器

调度器结构,各个调度算法大同小异,根本差异还是在调度策略上

基于设计目标,在复杂性,吞吐量,实时性之间去做取舍

Erlang产生背景(通信领域),设计之初就对实时性(低延迟)非常看重

为达成软实时性,调度必然是抢占式的,给每个Erlang进程设定规约(Reduction)来作为一个进程的虚拟时间片

进程调用函数,BIF,GC,ETS操作,发送消息等,都会消耗规约,甚至用Erlang自带的用C写的正则表达式处理,也添加了扣除规约的代码

每个Erlang进程默认有2000规约,Erlang设计理念中,天下没有”免费”的操作,重度依靠规约来衡量进程何时被换出

但理想和现实往往有差别,NIF(用户用C实现的可供Erlang调用的函数)的出现打破了这个定律,它是不可被调度的,对此,Erlang打出了如下补丁:

  • 官方建议NIF不要超过1ms(相信开发者对自己写的代码心里有数…) = 在NIF中给Erlang虚拟机手动汇报当前执行时间,并手动记录上下文和恢复(抢占式变成了协作式,强制变成了自愿…)
  • 将NIF放到自己创建的OS线程中执行,通过消息的方式将结果返回到Erlang进程(走开,别脏了我的公平调度器)
  • 将NIF放到OTP为你准备的脏调度器(OTP R17引入,需要在启动时通过参数开启)中执行(好吧,我给你提供脏调度器)

以上方案治标不治本,所以写纯Erlang代码,你可以享受到它很多的便利,但是一旦你因为性能问题,或要对接外部库等各种原因需要用到C的时候,一切都开始不美好了

Erlang 的另一个问题也来自于其”公平调度”

假设有10000个逻辑进程,将日志数据发到一个logger进程,那么系统上一共有10001个进程(忽略其它)

理想情况下,希望这个logger有任务就处理,避免消息堆积,最大化吞吐量

但Erlang的公平调度会想尽办法让这个logger进程和其它逻辑进程平起平坐,哪怕它有很多事情要做,然后导致导致logger进程消息队列膨胀,内存随之增长,甚至VM随之挂掉。针对这个问题通常的处理方案是:

  • 提升logger进程的优先级(没有具体测试过)
  • 开多个logger进程争取更多的执行权(笨办法)
  • 从设计上尽量避免这种扇入扇出模型,同一节点尽可能跑相同类型的任务,比如将logger放到其它节点(Erlang的”并发思维”)
  • 通过NIF来绕过时间片限制(骚操作)

因此,通常说Erlang在进程数少的时候表现不怎么样,而在进程多的却有很好的低延时表现。Erlang 调度器还有一些其它细节没有提到,比如:

  • 通过三个优先级队列(low/normal,high,max)来实现进程的优先级调度
  • 当调度器idle时,会自旋一段时间,如果没有新任务到达,则关闭该调度器(节能减排)
  • 除了Process外,Erlang还处理Port任务,Port是Erlang与外部通信(文件,网络,C等)的一种机制
  • Erlang对网络IO进行了特别的优化(System Level Activities),即将所有的套接字设置为非阻塞,然后通过epoll机制去轮询并唤醒调用进程,通过应用层的阻塞/唤醒模拟系统调用

go 调度

如果说 Erlang 为实时性殚精竭虑,Go 实务得多,更加偏向于吞吐量和实用性

Go GPM模型,没有时间片或规约的概念,它的抢占也不是完全抢占的,通过后台线程sysmon来监控并决定何时发起抢占,何时 GC 等

比如一个goroutine执行超过了10ms,sysmon向其发出抢占请求。抢占的方式也很简单,给goroutine打一个标记,goroutine在调用函数分配函数栈时会检查该标记,来决定当前G是否应该让出调度权,因此它没办法抢占死循环

可以看到,Go的调度模型实现相对比较简单,一方面可能调度器仍然还很年轻(STW的痛还历历在目),另一方面Go的设计哲学就是简单。如果要解释得再官方一些: Go 更看重吞吐量,而非实时性

Erlang和Go都是为服务器设计的语言,都对网络IO进行了优化,所有socket nonblock,通过轮询/唤醒来向应用层屏蔽系统调用,通过IO复用+应用层阻塞来避免调度器阻塞在系统调用上

最后

要理解一个调度器,需要结合其产生背景,设计目标,变更历史等

复杂的系统,通常都是”First make it work, then measure, then optimize”,比如 Erlang 的 NIF,Go 的 STW 等,都在逐渐优化

实时性和吞吐量是不可兼得的,已经在其它系统上得到了验证

实时系统通常实现都比较复杂,并且由于现代 OS 最多满足软实时特性,应用层的调度也最多实现软实时

严格意义上的硬实时系统通常由特定领域的嵌入式系统实现。同时,也因为应用层的调度受更底层的OS调度的影响,要达到一个系统的整体最优,需要协调不同的调度层级,比如 Erlang 提供一个+sbt参数可以将调度器与 CPU 核心绑定,这样可以更好地利用 CPU 缓存和局部性,以提升整体性能

转载于:https://my.oschina.net/zhangthe9/blog/3021436

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值