进程、线程和协程:
进程:资源分配的基本单元,有自己独立的内存空间,进程间通信需要通过各种通信机制
线程: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可以抢先进来执行
参考: