知方号

知方号

GolangGMP模型 GMP(二):goroutine的创建,运行与恢复

GMP二 前言理解

前言

GolangGMP模型 GMP(一):HelloWorld程序的执行过程 GolangGMP模型 GMP(二):goroutine的创建,运行与恢复 GolangGMP模型 GMP(三):协程让出,抢占,监控与调度 GMP总结—>深入理解GMP模型

理解

还是这个hello goroutine的例子,只不过给协程入口增加了一个参数,我们已经知道main goroutine执行起来会创建一个hello goroutine,而创建的任务,就交由newproc函数来负责。

我们通过函数栈帧看一下newproc函数的调用过程,main函数栈帧自然分配在main goroutine的协程栈中 ,还记得go语言函数栈帧布局吧,call指令入栈的返回地址之后 ,是调用者栈基,然后是局部变量区间,以及调用其他函数时,传递是返回值和参数的区间,main函数这里有一个局部变量name。接下来要调用newproc函数,从newproc函数签名来看,要接收两个参数,第一个是传递给协程入口函数的参数占多少字节,第二个是协程入口函数对应的funcval指针,所以在参数空间这里要入栈两个参数,先入栈fn,也就是协程入口函数hello对应的function val指针,再入栈参数大小,一个string类型的参数在64为的情况下占16字节,实际上者个要传递给协程入口函数的参数name,也会被放到第二个参数之后,所以要将局部变脸name拷贝到栈里(由右到左,实际上是第一个入栈的),这样就好像给newproc函数传递了三个参数一样。

而这下面就是Call指令入栈的返回地址,然后是调用者bp,而newproc函数这里主要做的就是切换到g0栈去调用newproc1函数,至于为什么要切换到g0栈,简单来说是因为g0的栈空间它大!因为runtime中很多函数都有no-split标记,意味着这个函数不支持栈增长,也就是说编译器不会在这个函数中插入栈增长相关的检测代码,协程栈本来就比线程栈小的多,这些个函数自己要消耗栈空间,却又不支持栈增长,那在普通协程上执行它们,万一栈溢出了就不好了,而g0的栈直接分配在线程栈上,栈空间足够大。所以直接切换到g0栈来执行这些函数,就不用担心栈溢出的问题了。

再来看它调用newproc1时,都传递了些什么参数,fn和siz不用多说,就是newproc自己接收的那两参数,这个argp赋值时,使用参数fn的地址加上一个指针大小,那就正好是参数name,所以argp用来定位到协程入口函数的参数,第四个参数是当前协程的指针,在这个例子中,newproc函数执行在main goroutine中,所以gp就是main goroutine的g指针,最后一个参数pc,在newproc函数中调用getcallerpc(),得到的是newproc函数调用结束后的返回地址,也就是return addr(由call指令入栈的那个返回地址)。

于newproc1而言,它的任务是创建一个协程,而目前已经知道新创建协程入口在哪里了 ,参数在哪,参数大小,父协程,以及创建完协程后要返回到哪去。

newproc1首先通过acquirem静止当前m被抢占,为什么呢?因为接下来要执行的程序中,可能会把当前p保存到局部变量中,若此时m被抢占,p关联到别的m,等到再次恢复时,继续使用这个局部变量里保存的p ,就会造成问题,所以为了保持数据一致性,会暂时禁止m被抢占。接下来会尝试获取一个空闲的G,如果当前p和调度器中都没有空闲的G,就创建一个,并添加到全局变量allgs中,我们依然把这个新创建的协程记为hello goroutine。此时它的状态是_Gdead,而且已然拥有自己的协程栈。如果协程入口函数有参数,就把参数移动到协程栈上,对hello goroutine而言,就要把参数name拷贝到hello goroutine stack上接下来会把goexit函数的地址加1,压入协程栈 再把hello goroutine对应的g这里,startpc置为协程入口函数起始地址(fn),gopc置为父协程调用newproc后的返回地址,g.sched这个结构体用于保存现场,此时会把g.sched.sp置为协程栈指针,g.sched.pc指向协程入口函数的起始地址fn。

现在我们来看hello goruotine的协程栈,name是参数,&goexit+1是返回地址,下面是hello函数的栈帧。==其实就“伪装”成了在goexit函数中调用了协程入口函数hello(name),并传递了用户传递的参数,而指令指针刚刚跳转到hello函数的入口处,却还没有开始执行时的状态,所以经这一通伪装,待到这个协程得到调度执行的时候,通过g.sched,就会从hello函数入口处开始执行了,而hello函数结束后便会返回到goexit函数中,执行协程资源回收等收尾工作。==这样一来协程该如何出场,该如何收场,就都有了着落,甚是巧妙。

不过这还没完,newproc1还会给新建的goroutine赋予一个唯一id,给g.goid赋值前,会把协程的状态置为_Grunnable,这个状态意味着这个G可以进入到run queue里了,最后与一开始的acquirem相呼应,调用releasem允许当前m被抢占。所以接下来会调用runqput把这个G放到P的本地队列中。

如果当前有空闲的p,而且没有处于spinning状态(线程自旋)的M,也就是说所有M都在忙,同时主协程以及开始执行了,那么就调用wakep函数,启动一个m并把它置为spinning状态。spinning状态的M启动后,忙不迭的执行调度循环寻找任务,从本地runq,到全局runq,再到其他p的runq,只为找到一个待执行的G,却也敌不过另一边,main goroutine早早的结束了进程。

上一次我们使用time.Sleep,拖延了一个main.main返回的时间,这一次我们通过等待一个channel,看一下协程如何让出又是如何恢复的。channel对应的数据结构是runtime.hchan,里面有channel缓冲区地址,大小,读写下标,也记录着元素类型,大小,及channel是否已关闭,还记录着等待channel的那些G的读队列和写队列,自然也少不了保护channel线程安全的锁。我们的例子中创建的channel是无缓冲的。

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌抄袭侵权/违法违规的内容, 请发送邮件至lizi9903@foxmail.com举报,一经查实,本站将立刻删除。