单线程进程的缺点
- 只有执行完一个任务才可以执行另外一个
- cpu资源浪费,万一一个进程阻塞了(单进程没有切换能力)
多线程、进程的缺点
- 切换成本 (cpu成本浪费,内存也是高消耗,一个进程虚拟内存接近4GB)
- 开发设计变得复杂(同步竞争 )
调度器1:N模型
无法利用多个cpu,出现阻塞现象
1:N 有一个阻塞了,全都挂了,那我8核cpu,8个阻塞了,那就挂了?
老调度器
-
创建销毁调度G,都需要每个M获得锁,造成了激烈的锁竞争
-
M转移G造成额外的系统负载(G里面创建G‘ 可能被其他线程执行)
-
CPU在M之间切换,导致频繁的线程阻塞和取消,增加了系统开销
GMP模型概念简介
G goroutine 协程
P processor 处理器
M thread 线程
全局队列:存放等待运行的G
P的本地队列:数量限制,有限将新创建的G放到p的本地队列中,满了放到全局队列
P列表:程序启动时创建,GOMAXPROCS可以配置(环境变量)
M列表:当前操作系统分配到GO程序的内核线程数量
调度器的设计策略
- 复用线程(尽量不进行创建或者销毁线程)
work stealing机制 (出现没有P队列的M时,分一个)
hand off机制(出现阻塞时,将队列和P寻找别的空闲或者休眠的M执行)
- 利用并行
可以限定P的个数(来保证并行最大的数量)
- 抢占
co-routine需要主动释放cpu不然就会阻塞,goroutine是一个抢占式模型(例如每个g最多等待10ms)
- 全局G队列
如果其他p的本地队列也没有G了,就会从全局队列中获取(当然需要加锁和解锁)
go func经历了什么?
1、我们通过go func()来创建一个goroutine
2、有两个存储G的队列,一个是局部调度器P的本地队列、一个是全局G队列。新创建的G会先
保存在P的本地队列中,如果P的本地队列已经满了就会保存在全局的队列中;
3、G只能运行在M中,一个M必须持有一个P,M与P是1:1的关系。M会从P的本地队列弹出一个可执行状态的G来执行,如果P的本地队列为空,就会想其他的MP组合偷取一个可执行的G来执行
4、一个M调度G执行的过程是一个循环机制(调度,执行,销毁,返回)
5、当M执行某一个G时候如果发生了系统调用或则其余阻塞操作,M会阻塞,如果当前有一些G在执行,runtime会把这个线程M从P中摘除(detach),然后再创建一个新的操作系统的线程(如果有空闲的线程可用就复用空闲线程)来服务于这个P
6、当M系统调用结束时候,这个G会尝试获取一个空闲的P执行,并放入到这个P的本地队列。如果获取不到P,那么这个线程M变成休眠状态,加入到空闲线程中,然后这个G会被放入全局队列中。
执行go进程是会发什么什么?
M0: 启动程序后编号为0的主线程,在全局变量runtime.m0中,不需要再heap上进行分配。
负责初始化操作和第一个G,启动完成第一个G之后,M0就和其他M一样了。
G0:每次启动一个M,都会创建一个goroutinue,就是G0。G0是用来调度G的(M先切换到G0,使用G0来调度其他G)。M0的G0 会放再全局空间
场景
场景一
假如G1创建G3,优先创建到G1所在的本地队列中。(假如创建过多,不只有G3一个,并且G1所在的本地队列放不开,考虑将本地队列分成两半,前一半打散放到全局队列中,新创建的也放到全局队列中,后一半往前移)
场景二
优先执行本地队列的G
场景三
当创建一个G时,就会尝试唤醒一个thread
场景四
被唤醒的线程从全局队列批量获取G
场景五
全局队列中没有G,那么m就要进行work stealing(偷取),从其他G的P的队列中的偷取后半部分G过来,放到自己的P本地队列。
场景六
自旋线程+执行线程<=GoMAXPROCS
场景七
G发生系统调用时,或者阻塞时,M和P进行解绑,唤醒一个新的M与P进行绑定
场景八
G由阻塞转换为非阻塞时间,优先找到上一个分离的p,如果不是空闲就去全局的p队列中找,如果还是没找到p,G就会放到全局队列,M就会放到休眠队列等待调用