Goroutine的调度

490 阅读5分钟

进程、线程和协程:

进程:资源分配的基本单元,有自己独立的内存空间,进程间通信需要通过各种通信机制

线程:cpu调度的基本单元,运行在同一个进程内的线程共享进程的资源,线程间切换代价小

协程:用户级别的线程,相对于线程的由操作系统调度,协程的调度完全由用户控制

Goroutine非常轻量级:

1.上下文切换代价小,goroutine切换时调度器只需要保存和恢复三个寄存器,程序指针,栈指针和dx,开销比较小;而线程阻塞时,另一个线程需要被调度到当前处理器上运行,切换线程时调度器需要保存/恢复所有寄存器,包括16个通用寄存器,程序指针,栈指针等(只有运行的goroutine才会被考虑切换,阻塞的会被忽略)

2.内存占用小,线程栈空间一般是2M?8M,goroutine只需要2k

3.创建和销毁小,goroutine的创建和销毁是由运行环境(runtime)完成的

Golang采用轻量级的goroutine来实现并发,大大减少CPU的切换

Go的调度机制:

P(Processor)由任务时,需要唤醒一个系统线程来执行它队列里面的任务,所以P和M需要进行绑定,构成一个执行单元

P决定了可以同时并发任务的数量,可以通过runtime.GOMAXPROCS限制同时执行用户级任务的操作系统线程,并且函数返回之前的值

1、P有两种队列,本地队列和全局队列

本地队列:当前P的队列,没有数据竞争

全局队列:为了保证多个P之间任务的平衡,所有M共享P全局队列,为保证数据竞争问题,需要加锁处理

2、上下文切换

上下文即当时运行的环境,包括寄存器的值

3、线程清理 只需要将P进行释放,P被释放的情况有两种:1)主动释放:当执行G任务时有系统调用,此时M会处于阻塞(Block)状态。调度器会设置一个超时时间,当超时时会将P释放。2)被动释放:当发生系统调用时,有一个专门的监控程序,进行扫描当前处于阻塞的P/M组合。当超过系统程序设置的超时时间,会自动将P资源抢走,去执行队列的其它G任务

Goroutine中的三个实体:

G:代表一个goroutine对象,每次go调用时,会创建一个G对象,它包括栈、指令指针以及调用goroutine的其它重要信息,比如阻塞它的channel

type g struct {
	sched	gobuf //goroutine切换时,用于保存g的上下文(当前栈指针、计数器、g自身)
}

type gobuf struct {
	sp	uintptr
    pc	uintptr
    g	uintptr
    ctxt	unsafe.Pointer
    ret		sys.Uintreg
    lr		uintptr
    bp		uintptr
}

M:代表一个线程,每次创建一个M时,都会有一个底层线程创建,所有的G任务,最终在M上执行

type m struct {
	g0			*g			//一个特殊的goroutine,栈是M对应的线程的栈,也就是说线程的栈也是由g实现的,而不是使用的OS
    gsignal		*g
    curg		*g			//当前运行的goroutine
    p			puintptr	//关联p和执行的go代码
    
    mallocing	int32		//状态
    spinning	bool
    blocked		bool		//m是否被阻塞
    inwb		bool
}

P:代表一个处理器,每一个运行的M必须绑定一个P,P的个数就是GOMAXPROCES(最大256),启动时固定的,一般不修改(逻辑cpu个数),M和P的个数不一定一样多(会有休眠的M或不需要太多的M)(最大10000),每个P保存着本地G任务队列,也有一个全局的G任务队列

type p struct {
	status		uint32		//状态,可以为pidle、prunning、psyscall、pgcstop、pdead
    m			muintptr	//回链到关联的m
    
    //可运行的goroutine队列
    runqhead	uint32
    runqtail	uint32
    run1		[256]guintptr
    
    runnext		guintptr	//下一个运行的g
    
}

全局调度者

type schedt struct {
	lock	mutex
    
    //全局的可运行的g队列
    runqhead	guintptr
    runqtail	guintptr
    runqsize	int32
}

goroutine的运行过程:

1)调用newproc() 创建一个g对象 2)初始化结构体上的一些域 3)将g挂在就绪队列 绑定g到一个m上(只要m没的突破GOMAXPROCES上限,就拿一个m绑定一个g)

2)调用newm()创建一个m,m在底层就是创建一个线程

3)m创建好之后,线程的入口是mstart

func mstart1() {
	...
    schedule()
}

func schedule() {
	//找到一个等待运行的g,搬到m上,设置其状态为gruning,直接切换到g的上下文环境,恢复g的执行
}

go调度器采取了以下几种调度策略

1.任务窃取:

为了提高go并行处理能力,当每个P之间的G任务不均衡时,调度器允许从GRQ(全局运行队列)或者其它P的LRQ(本地运行队列)中获取G执行

2.减少阻塞:go里面阻塞主要份4个场景

1)由于channel、原子、互斥量操作调用导致的阻塞:调度器将把当前阻塞的goroutine切换出去,重新调度LRQ上的其它goroutine

2)由于网络请求和IO操作导致goroutine阻塞:Go提供了网络轮询器(NetPoller)来处理网络请求和IO操作的问题,后台通过epoll(Linux)实现IO多路复用

G被移到了NetPoller上进行异步的网络系统调用,M可以执行P中其它的goroutine,网络系统调用完成后,G被移回到了P的LRQ中

3)调用一些系统方法时发生阻塞:goroutine导致M阻塞,此时调度器将M和P分离,分离后的P和新的M绑定,继续调度GRL中的goroutine,阻塞的系统调用完成后,可以移回LRQ并再次由P执行

4)goroutine中执行sleep操作导致M被阻塞:go程序后台有一个监控线程sysmon,它监控哪些长时间运行的G任务,然后设置可以强占的标识符,别的goroutine可以抢先进来执行

参考:

www.cnblogs.com/sunsky303/p…

zhuanlan.zhihu.com/p/28381197

studygolang.com/articles/27…