goroutine基础

一、goroutine是什么?
是go里的一种轻量级线程-协程

1、相对线程,协程的优势在于它非常轻量级,进行上下文切换的代价非常小

2、相对于一个goroutine,每个结构中有一个sched的属性就是用来保存它上下文的,这样,goroutine就可以很轻易的来回切换

3、由于其上下文切换在用户态下发生,根本不必进入内核态,所以速度很快。而且只有当前goroutine的pc,sp等少量信息需要保存

4、在go语言中,每一个并发的执行单元为一个goroutine

go语言中的goroutine并发,采用的是CSP(communicating sequential processes)并发模型,讲究的是以通讯的方式来进行数据共享,是通过goroutine配合channel的方式来实现的。

二、既然它是比线程还小的粒度,那么它与线程有什么关系?

go语言的线程模型就是一种特殊的两级线程模型,如下图所示:

goroutine基础

两级线程模型的实现非常复杂,和内核级线程模型类似,一个进程中可以对应多个内核级线程,但是进程中的线程不和内核线程一一对应;这种线程模型会先创建多个内核级线程,然后用自身的用户级线程去对应创建的多个内核级线程,自身的用户级线程需要本身去调度,内核级的线程交给操作系统内核去调度

三、goroutine的调度策略

1、介绍一下go线程实现的MPG模型:

S(Sched):这就是调度器,它维护有存储M和G的队列以及调度器的一些状态信息等

M(Machine):一个M直接关联了一个内核线程

P(Processor):代表了M所需要的上下文环境,也是处理用户级代码逻辑的处理器

G(goroutine):其本质上也是一种轻量级的线程

它们的关系如下:

goroutine基础

对上图进行阐述:

(1)一个M 会关联两个东西,一个是内核线程,一个是可执行的进程

(2)一个上下文P会有两类Goroutine,一类是正在运行的,图中的蓝色G;一类是正在排队的,图中灰色的G,这个会存储在该进程中的runquenue里面

(3)这里的上下文P的数量也表示的是Goroutine运行的数量,一般设置为几个,机器中就会并发运行几个,当然这里的P的是可以设置的,通过环境变量GOMAXPROCS的值,或者通过运行时调用函数runtime.GOMAXPROCS()进行设置,最大值是256

四、go的并发是如何调度的这一话题,需要我们将Goroutine的几种场景(创建、销毁和运行)做拆分

1、在执行go语句之前,先看一下程序都做了哪些准备,也就是程序的初始化启动流程是什么样的?

goroutine基础

上面的代码,有三个点非常关键,分别是runtime.schedinit,runtime.main,runtime.mstart

(1)runtime.schedinit:这一步是调度器的初始化操作,它会设置GOMAXPROCS的大小,这里的大小不能超过它的上限256,并创建设置好对应数量的P,当然这些P都处于闲置状态;然后,将这些创建好的P都存放到sched中pidle所关联的闲置列表中。

(2)程序会继续执行runtime.newproc来创建程序的第一个goroutine,而这个goroutine会执行runtime.main也就是我们看到的main函数,在这之后main会主动创建一个内核线程M,这个M只用来做系统监控用,这个内核线程与程序中goroutinue的调度有关系。

(3)在runtime.mstart之后,程序就开始执行了,如果后续需要创建goroutine,就会调用go语句来创建。

2、goroutine创建流程是什么样子的?

在调用go func()的时候,会调用runtime.newproc来创建一个goroutine,这个goroutine会新建一个自己的栈空间,同时在G的sched中维护栈地址与程序计数器这些信息(备注:这些数据在goroutine被调度的时候会被用到。准确的说该goroutine在放弃cpu之后,下一次在重新获取cpu的时候,这些信息会被重新加载到cpu的寄存器中。)创建好的这个goroutine会被放到,它所对应的内核线程M所使用的上下文P中的runqueue中。等待调度器来决定何时取出该goroutine并执行,通常调度是按时间顺序被调度的,这个队列是一个先进先出的队列。

3、新建的这些goroutine是如何被调度的呢?

goroutine在创建好了之后,调度器会决定何时执行这个goroutine,这个过程就叫做调度。新建好的goroutine,最开始都会存储在某一个线程M,所关联的上下文P的runqueue中,但是在后续的调度中,有些goroutine因为调用了runtime.gosched,会被放到全局队列中。

线程M的选择过程,按照下面的顺序执行:

(1)从M对应的P中的runqueue中取出goroutine,来执行,没有的话,执行2。
(2)从全局队列里面尝试取出一个goroutine来执行,有的话,执行!没有的话,执行3。
(3)从其他的线程M的P中,偷出一些goroutine来执行,偷失败了,执行4。(备注:这里偷的话,一偷就偷一半,使用的算法叫做work stealing。)
(4)线程M发现无事可做,就去休息了,也就是线程的sleep,它等待被唤醒。

4、运行中的goroutine是怎么停止的呢?一旦被停止了的话,那排队在它后面的goutine该怎么办呢?(讲完了goroutine的调度之后,我们便要考虑一个问题,正在被执行的goroutine何时停止,停止了之后会发生什么?而挂在M对应的P后面的runqueue中的goroutine该怎么办?)

情况1: runtime.park

当调用了runtime·park函数之后,goroutine会被设置成waiting状态,线程M会放弃它自身关联的上下文P,而系统会分配一个新的线程M1来接管这个上下文P,(备注:当然这里面的M1也有可能是本来就创建好的,处于闲置状态中的)。原来的线程M0则会与上下文断开连接,M0因为无事可做,就去sleep了,等待下次被唤醒。如下图所示:channel的读写操作,定时器中,网络poll等都有可能park goroutine。

goroutine基础

 

情况2:runtime.gosched

调用runtime·gosched函数也可以让当前goroutine放弃cpu,这种情况下会将goroutine设置称runnable,放置到全局队列中。备注:这个也就是为什么全局变量的queue里面会有goroutine的原因。

5、goroutine被唤醒之后,会做什么?

goroutine处于waiting状态的话,在调用runtime·ready函数之后,会被唤醒,唤醒的goroutine会被重新放到,M对应的上下文所对应的runqueue中,等待被调度。

goroutine基础