内容简介:本文从宏观角度介绍了一下Go调度器的调度过程。上篇文章回顾:
本文从宏观角度介绍了一下 Go 调度器的调度过程。
上篇文章回顾: Etcd+confd通过Nginx对后端服务的注册发现
前言
随着服务器硬件迭代升级,配置也越来越高。为充分利用服务器资源,并发编程也变的越来越重要。在开始之前,需要了解一下并发(concurrency)和并行(parallesim)的区别。
并发: 逻辑上具有处理多个同时性任务的能力。
并行: 物理上同一时刻执行多个并发任务。
通常所说的并发编程,也就是说它允许多个任务同时执行,但实际上并不一定在同一时刻被执行。在单核处理器上,通过多线程共享CPU时间片串行执行(并发非并行)。而并行则依赖于多核处理器等物理资源,让多个任务可以实现并行执行(并发且并行)。
多线程或多进程是并行的基本条件,但单线程也可以用协程(coroutine)做到并发。简单将Goroutine归纳为协程并不合适,因为它运行时会创建多个线程来执行并发任务,且任务单元可被调度到其它线程执行。这更像是多线程和协程的结合体,能最大限度提升执行效率,发挥多核处理器能力。
Go编写一个并发编程程序很简单,只需要在函数之前使用一个Go关键字就可以实现并发编程。
func main() { go func(){
fmt.Println("Hello,World!")
}()
}
Go调度器组成
Go语言虽然使用一个 Go 关键字即可实现并发编程,但Goroutine被调度到后端之后,具体的实现比较复杂。先看看调度器有哪几部分组成。
1、G
G是 Go routine的缩写,相当于操作系统中的进程控制块,在这里就是Goroutine的控制结构,是对Goroutine的抽象。其中包括执行的函数指令及参数;G保存的任务对象;线程上下文切换,现场保护和现场恢复需要的寄存器(SP、IP)等信息。
Go不同版本Goroutine默认栈大小不同。
// Go1.11版本默认stack大小为2KB
_StackMin = 2048
// 创建一个g对象,然后放到g队列
// 等待被执行
func newproc1(fn *funcval, argp *uint8, narg int32, callergp *g, callerpc uintptr) {
_g_ := getg()
_g_.m.locks++
siz := narg
siz = (siz + 7) &^ 7
_p_ := _g_.m.p.ptr()
newg := gfget(_p_)
if newg == nil {
// 初始化g stack大小
newg = malg(_StackMin)
casgstatus(newg, _Gidle, _Gdead)
allgadd(newg)
}
// 以下省略}
2、M
M是一个线程或称为Machine,所有M是有线程栈的。如果不对该线程栈提供内存的话,系统会给该线程栈提供内存(不同操作系统提供的线程栈大小不同)。当指定了线程栈,则M.stack→G.stack,M的PC寄存器指向G提供的函数,然后去执行。
type m struct {
/*
1. 所有调用栈的Goroutine,这是一个比较特殊的Goroutine。
2. 普通的Goroutine栈是在Heap分配的可增长的stack,而g0的stack是M对应的线程栈。
3. 所有调度相关代码,会先切换到该Goroutine的栈再执行。
*/
g0 *g
curg *g // M当前绑定的结构体G
// SP、PC寄存器用于现场保护和现场恢复
vdsoSP uintptr
vdsoPC uintptr
// 省略…}
3、P
P(Processor)是一个抽象的概念,并不是真正的物理CPU。所以当P有任务时需要创建或者唤醒一个系统线程来执行它队列里的任务。所以P/M需要进行绑定,构成一个执行单元。
P决定了同时可以并发任务的数量,可通过GOMAXPROCS限制同时执行用户级任务的操作系统线程。可以通过runtime.GOMAXPROCS进行指定。在Go1.5之后GOMAXPROCS被默认设置可用的核数,而之前则默认为1。
// 自定义设置GOMAXPROCS数量
func GOMAXPROCS(n int) int {
/*
1. GOMAXPROCS设置可执行的CPU的最大数量,同时返回之前的设置。
2. 如果n < 1,则不更改当前的值。
*/
ret := int(gomaxprocs)
stopTheWorld("GOMAXPROCS")
// startTheWorld启动时,使用newprocs。
newprocs = int32(n)
startTheWorld()
return ret
}
// 默认P被绑定到所有CPU核上
// P == cpu.cores
func getproccount() int32 {
const maxCPUs = 64 * 1024
var buf [maxCPUs / 8]byte
// 获取CPU Core
r := sched_getaffinity(0, unsafe.Sizeof(buf), &buf[0])
n := int32(0)
for _, v := range buf[:r] {
for v != 0 {
n += int32(v & 1)
v >>= 1
}
}
if n == 0 {
n = 1
}
return n
}
// 一个进程默认被绑定在所有CPU核上,返回所有CPU core。
// 获取进程的CPU亲和性掩码系统调用
// rax 204 ; 系统调用码
// system_call sys_sched_getaffinity; 系统调用名称
// rid pid ; 进程号
// rsi unsigned int len
// rdx unsigned long *user_mask_ptr
sys_linux_amd64.s:
TEXT runtime·sched_getaffinity(SB),NOSPLIT,$0
MOVQ pid+0(FP), DI
MOVQ len+8(FP), SI
MOVQ buf+16(FP), DX
MOVL $SYS_sched_getaffinity, AX
SYSCALL
MOVL AX, ret+24(FP)
RET
Go调度器调度过程
首先创建一个G对象,G对象保存到P本地队列或者是全局队列。P此时去唤醒一个M。P继续执行它的执行序。M寻找是否有空闲的P,如果有则将该G对象移动到它本身。接下来M执行一个调度循环(调用G对象->执行->清理线程→继续找新的Goroutine执行)。
M执行过程中,随时会发生上下文切换。当发生上线文切换时,需要对执行现场进行保护,以便下次被调度执行时进行现场恢复。Go调度器M的栈保存在G对象上,只需要将M所需要的寄存器(SP、PC等)保存到G对象上就可以实现现场保护。当这些寄存器数据被保护起来,就随时可以做上下文切换了,在中断之前把现场保存起来。如果此时G任务还没有执行完,M可以将任务重新丢到P的任务队列,等待下一次被调度执行。当再次被调度执行时,M通过访问G的vdsoSP、vdsoPC寄存器进行现场恢复(从上次中断位置继续执行)。
1、P 队列 通过上图可以发现,P有两种队列:本地队列和全局队列。
-
本地队列: 当前P的队列,本地队列是Lock-Free,没有数据竞争问题,无需加锁处理,可以提升处理速度。
-
全局队列: 全局队列为了保证多个P之间任务的平衡。所有M共享P全局队列,为保证数据竞争问题,需要加锁处理。相比本地队列处理速度要低于全局队列。
2、 上线文切换
简单理解为当时的环境即可,环境可以包括当时程序状态以及变量状态。例如线程切换的时候在内核会发生上下文切换,这里的上下文就包括了当时寄存器的值,把寄存器的值保存起来,等下次该线程又得到cpu时间的时候再恢复寄存器的值,这样线程才能正确运行。
对于代码中某个值说,上下文是指这个值所在的局部(全局)作用域对象。相对于进程而言,上下文就是进程执行时的环境,具体来说就是各个变量和数据,包括所有的寄存器变量、进程打开的文件、内存(堆栈)信息等。
3、线程清理 Goroutine被调度执行必须保证P/M进行绑定,所以线程清理只需要将P释放就可以实现线程的清理。什么时候P会释放,保证其它G可以被执行。P被释放主要有两种情况。
-
主动释放: 最典型的例子是,当执行G任务时有系统调用,当发生系统调用时M会处于Block状态。调度器会设置一个超时时间,当超时时会将P释放。
-
被动释放: 如果发生系统调用,有一个专门监控程序,进行扫描当前处于阻塞的P/M组合。当超过系统程序设置的超时时间,会自动将P资源抢走。去执行队列的其它G任务。
总结
本文从宏观角度介绍了一下Go调度器的调度过程。Go调度器也是Go语言最精华的部分,希望对大家有所帮助。
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持 码农网
猜你喜欢:- Java多线程并发:进程调度算法
- GO 中的调度:第三部分 - 并发
- Golang 并发问题(五)goroutine 的调度及抢占
- Goroutine 并发调度模型深入之实现一个协程池
- Golang实现简单爬虫框架(4)——队列实现并发任务调度
- Golang随谈——浅瞰底层:Go的并发调度模型
本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。
改变未来的九大算法
[美] 约翰.麦考密克 / 管策 / 中信出版社 / 2013-6 / 39.00元
Google得出的搜索结果是如何产生的? 百度为何会陷入“搜索门”,又是什么机制使然? 身处在大数据时代的我们,究竟该如何应对变化莫测的世界? …… 没有满篇的专业术语,第一次让我们通过简单明了的语言、生动的例证了解支撑计算机王国的灵魂支柱——9大算法,包括人工智能、数据压缩,以及Google著名的PageRank等。 本书精彩地介绍了搜索引擎、PageRank、公开......一起来看看 《改变未来的九大算法》 这本书的介绍吧!