go协程
协程需要解决线程遇到的几个问题:
- 内存占用要小,且创建开销要小
- 减少上下文切换的开销
第一点好实现,用户态的协程,只是一个数据结构,无需系统调用,而且可以设计的很小,达到 KB 级别。
第二点只能减少上下文切换次数来解决,因为协程的本质还是线程,其切换开销在用户态是无法降低的,只能通过降低切换次数来达到总体上开销的减少,可以有如下手段:
- 让可执行的线程尽量少,这样切换次数必然会少
- 让线程尽可能的处于运行状态,而不是阻塞让出时间片
Go语言为了更易用、高效的并发能力,提出了goroutine的概念。通常情况下,一个goroutine只占用几kb的内存,远低于内核线程(几M),而且goroutine的整个生命周期都是由runtime包进行管理,不需要内核的参与。
- 并发,就是同时发生,而并行,就是同时进行。
- 以咖啡店为例,两人排两队同时点咖啡,而接到订单一个咖啡师做两杯咖啡,这是并发。两个咖啡师同时工作,每人做一杯咖啡,这是并行。同理回到程序中,多线程程序在单核上运行,就是并发;多线程程序在多核上运行,就是并行
多进程/线程虽然提高了系统的并发能力,但是一个进程/线程也占用了较多的内存(32位操作系统进程虚拟地址空间4GB,线程大约8MB),所以大量的进程/线程会带来以下2个问题:
- 高内存的占用
- 调度消耗大量CPU时间片
上面说的线程是内核线程,而程序直接管理的是处于用户态的线程 — 用户线程,也成为co-routine(协程),但是CPU调度的是内核线程,它感知不到用户线程的存在,所以用户线程需要调度器去调度。
- 在Go语言中通过自己的调度器实现了CSP模型,这个调度器也就是MPG调度模型:
先看 G,取 goroutine 的首字母,主要保存 goroutine 的一些状态信息以及 CPU 的一些寄存器的值,例如 IP 寄存器,以便在轮到本 goroutine 执行时,CPU 知道要从哪一条指令处开始执行。
再来看 M,取 machine 的首字母,它代表一个工作线程,或者说系统线程。G 需要调度到 M 上才能运行,M 是真正工作的人。结构体 m 就是我们常说的 M,它保存了 M 自身使用的栈信息、当前正在 M 上执行的 G 信息、与之绑定的 P 信息……
再来看 P-处理器,用来执行 goroutine,它维护了本地可运行队列,取 processor 的首字母,为 M 的执行提供“上下文”,保存 M 执行 G 时的一些资源,例如本地可运行 G 队列,memeory cache 等。
G 需要在 M 上才能运行,M 依赖 P 提供的资源,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时候如果发生了syscall或则其余阻塞操作,M会阻塞,如果当前有一些G在执行,runtime会把这个线程M从P中摘除(detach),然后再创建一个新的操作系统的线程(如果有空闲的线程可用就复用空闲线程)来服务于这个P;
6、当M系统调用结束时候,这个G会尝试获取一个空闲的P执行,并放入到这个P的本地队列。如果获取不到P,那么这个线程M变成休眠状态, 加入到空闲线程中,然后这个G会被放入全局队列中。