一、进程和协程

1.1、CSP模型?

CSP 模型是“以通信的方式来共享内存”,而不是传统的多线程通过共享内存来实现通信。

1.2、进程线程存在的问题:

  1. CPU高消耗
    • 切换线程上下文需要申请、销毁资源消耗时间高
  2. 内存高占用
    • 线程占用1M以上的内存空间

1.3、协程(Goroutine)的优点

  1. 占用的内存小(通常只有几kb)
    • 初始为2kb,如果栈空间不足则会自动扩容。
  2. 调度更灵活(runtime调度)
    • Go是自己实现调度器的,是用户及,通过P去绑定M,不直接操作线程,创建消耗非常小。
  3. 抢占式调度(10ms)
    • 编译器插入抢占指令,函数调用时检查当前Goroutine是否发起抢占请求
  4. 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()执行过程”!

请添加图片描述

总结:

  1. go关键字创建一个goroutine入队,如果本地P队列满了则入队全局G队列
  2. 从P队列中队头的G交给M执行
  3. P有两个关键特性
    1. work stealing(偷取机制)
    2. hand off(让出机制)