一、进程和协程
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(让出机制)