一、进程和协程
1.1、CSP模型?
CSP 模型是“以通信的方式来共享内存”,而不是传统的多线程通过共享内存来实现通信。
1.2、进程线程存在的问题:
- CPU高消耗
- 切换线程上下文需要申请、销毁资源消耗时间高
- 内存高占用
- 线程占用1M以上的内存空间
1.3、协程(Goroutine)的优点
- 占用的内存小(通常只有几kb)
- 初始为2kb,如果栈空间不足则会自动扩容。
- 调度更灵活(runtime调度)
- Go是自己实现调度器的,是用户及,通过P去绑定M,不直接操作线程,创建消耗非常小。
- 抢占式调度(10ms)
- 编译器插入抢占指令,函数调用时检查当前Goroutine是否发起抢占请求
- 1.14版本后支持基于信号的异步抢占(20ms)
- 垃圾回收扫描栈时触发抢占调度
- 解决抢占式调度因垃圾回收和循环长时间占用资源(无法执行抢占指令)导致程序暂停
二、Goroutine调度器的GMP模型的设计思想
-
在之前的go调度器是GM模型,没有P本地队列,会导致锁竞争严重,cpu的开销太大,后面加入了P
-
在新的调度器中,出列M(thread)和G(goroutine),又引进了P(Processor)。
Processor,它包含了运行goroutine的资源,如果线程想运行goroutine,必须先获取P,P中还包含了可运行的G队列。
1.1、GM老版调度器:
- 激烈的锁竞争:没有P模型的本地队列,只有全局队列,从全局队列中获取G就需要加锁,防止两个M抢到一个G。众所周知锁加多了会吃消耗cpu的性能。
- 局部性差:如果一个G中包含创建一个新协程的时候,G创建了G‘,为了继续执行G,需要把G’交给其他的M执行,但是G和G‘是相关联的,最好放到一个M上执行,而不是其他的M。
- 系统开销大:系统调用 (CPU 在 M 之间的切换) 导致频繁的线程阻塞和取消阻塞操作增加了系统开销。
1.2、GMP模型
GMP模型的基本结构
- G(goroutine)
- 类似操作系统中的线程
- 提供与用户态,粒度更小 ,切换代价更小
- 占用空间更小
type g struct {
stack stack // 当前G的栈范围
stackguard0 uintptr // 判读当前G是否被抢占
preempt bool // 抢占信号
preemptStop bool // 抢占时将状态修改成 `_Gpreempted`
preemptShrink bool // 在同步安全点收缩栈
_panic *_panic // 最内侧的 panic 结构体
_defer *_defer // 最内侧的延迟函数结构体
m *m // 当前G占用的线程
sched gobuf // 调度相关数据的存储
atomicstatus uint32 // G的状态
}
- M(Machine)
- P最多可以创建10000个线程,但是内核很难支持这么多的线程数,所以这个限制可以忽略。
- 最多只有GOMAXPROCS个活跃线程(与核数一致),这样不会频繁地切换线程上下文
type m struct {
g0 *g // 调度栈 使用的G
curg *g // 当前在M上运行的G
p puintptr // 正在运行代码的P
nextp puintptr // 暂存的P
oldp puintptr // 之前使用的P
}
- P(Processor)
- 调度线程上执行的G,可以让出那些等待资源(如网络、IO)的G,提高运行效率
- 同时提供M执行所需要的上下文环境以及资源
type p struct {
m muintptr // 调度的M
runqhead uint32 // G队列头
runqtail uint32 // G队列尾
runq [256]guintptr // G队列
runnext guintptr // 下一个可运行的G
status int // 当前P的状态
}
状态有以下几个取值
- _Pidle:运行队列为空,没有需要运行的G
- _Prunning:M正在执行用户G
- _Psyscall:M处于系统调用
- _Pgcstop:M处于GC垃圾回收的stop中
- _Pdead:P不再被使用
在Go中,线程是运行goroutinue的实体,调度器的功能是把可运行goroutinue分配到工作线程上。
runtime.GOMAXPROCS
1.3、GMP是可以无限扩增的吗(GMP分别的数量问题)?
- G(Goroutine):即Go协程,每个go关键字都会创建一个协程
- M(Machine):工作协程,在Go中称为Machine,go程序启动时,会设置M的最大数量默认10000。但是内核很难支持这么多的线程数,所以这个限制可以忽略。数量对应真实的CPU数(正儿八经的干活对象)
- P(Processor):处理器(Go中的概念),包含运行Go代码的必要资源,用来调度G和M之间的关联关系,其数量可通过GOMAXPROCS()来设置,默认为核心数。
M必须拥有P才可以执行G中的代码,P含有一个包含多个G的队列,P可以调度G交给M执行。
1.4、Goroutine调度策略和设计策略
调度策略:
-
队列轮转:P会周期性的将G调度到M 中执行,执行一段时间后,保存上下文,将G放到队列尾部,然后从队列中再取出一个G进行调度。除此之外,P还会周期性的查看全局队列是否有G等待调度到M中执行。
-
系统调用:当G0即将进入系统调用时,M0将释放P,进而某个空闲的M1获取P,继续执行P队列中剩下的G。M1的来源有可能是M的缓存池,也可能是新建的。
-
线程不会频繁的销毁和创建:如果M获取不到P,那么这个线程M变成休眠状态,加入到空闲线程中,等待被唤醒。
设计策略:
复用线程:避免频繁的创建、销毁线程,而是对线程的复用。
- 任务偷取(work stealing):全局队列已经没有 G,那 M 就要执行 work stealing (偷取):从其他有 G 的 P 哪里偷取一半 G 过来,放到自己的 P 本地队列。
- 让出执行权(hand off):某个G堵塞,线程释放绑定的P,把P转移给其它空闲线程
“Go func()执行过程”!
总结:
- go关键字创建一个goroutine入队,如果本地P队列满了则入队全局G队列
- 从P队列中队头的G交给M执行
- P有两个关键特性
- work stealing(偷取机制)
- hand off(让出机制)