美文网首页
自己对go协程的理解

自己对go协程的理解

作者: xyt001 | 来源:发表于2019-11-24 21:25 被阅读0次

    前言:

    下面是自己看了很多资料后总结下来的不一定对.因为能力还不够,还不能够深入源码一探究竟.
    如果自己哪里不对,希望可以留言.一起讨论一下.

    进程线程协程的理解

    1. 进程 :是计算机进行资源分配的最小单位,上下文开销很大,进程之间数据是隔离的.
    2. 线程 :是计算机调度的最小单位,是计算机运行的基本单元.(个人理解 可以把线程理解成是共享堆的进程),上
    下文调度相对于进程来说要轻量(因为线程之间调度不需要切换进程资源)
    3. 协程 :是用户态的一种轻量的线程,协程的调度完全由用户控制. 不需要内核参与,所以速度很快.
    

    go协程模型和特点

    协作顾名思义是互相协作的,但是go的协程不完全是互相协作的,还会相互抢占.go底层是基于gmp模型实现的协程调度.
    M: 系统真正的线程.m的任务就是想办法去找到可以运行的g.并且运行它
    P: G的集合,有个g的链表.一般会和m绑定.m不断运行p上面的g.并且p会存储m上线程的相关信息
    G: go的协程真正的实体.代表正在要执行的协程的逻辑和其要运行的上下文.
    

    自己对gmp模型的理解(不一定准确)

    大家有没有觉得m->p->g的关系其实就是 计算机->进程->线程的关系. 因为计算机涉及到很多因素的影响,所以对进程
    线程之间的调度开销会很大所以go的作者很有创造性的把这个过程在用户态自己模拟一套.正因为如此一些简单的调度都
    不需要用到系统来参与,go表示我自己就可以完成了,所以性能要好的很多!
    

    具体GMP是怎么运行的

    首先协程实现的功能和线程是一样的,就是让我们能够同时运行多个逻辑的能力(这里指的是并发不是并行).
    
    1. 启动协程: go程序内部都维护了一个 gfree的一个list.每次我们启动一个新的协程的时候 程序不会 
    马上就new一个g对象.而是先去gfree里找,如果gfree里面有现成的g对象,就会直接拿来用.如果gfree里面
    没有才会去new一个对象,所有的g运行完了都不会直接销毁.而是放到了gfree里.相当于gfree是一个g的对象池.
    总而言之go底层g是复用的.
    
    2. 协程绑定p: 当协程被启动后就会优先依附于启动自己的m所绑定的p上,这样很大程度上避免了m与m之间的竞争.
    当m上的p的g已经超过了承受范围就会放到全局的g的队列里.与m绑定的p最多只有256个g. 
    
    3. m的运行: m就是不断地运行依附于自己上的p上的g.为了让全局的g队列里也能够被调度到.m每运行61个本地g就会
    去全局g队列里拿一个g运行.当m本地的p没有g可以运行的时候也会去全局队列里面拿g运行.当全局队列也没有g的时候.
    就会去别的m绑定的p上偷一半g来运行.如果这个时候m还是拿不到g.那么这个m就会陷入睡眠.值得注意的是睡眠m也是
    不会被销毁的.防止m被频繁的创建销毁.
    

    go的协程 什么时候调度的,怎么调度的

    首先我们需要知道go的协作在什么情况会被调度(就是别的协程抢占)
    
    1. 当协程运行时间过长的时候,那么这个协程在调用非内联函数的时候会被调度:
    正式因为有这个特性,go的协程才具有了抢占性,才让goroutine不再是单纯的协程. 首先程序会在启动的时候运行一个
    sysmon线程,这个线程负责整个的调度具有上帝视角.sysmon会给所有的p都记录一个已运行的g的数量.每当p运行一个g后
    都会加1.如果这个变量没有加1,就说明这个p一直在运行同一个g任务.如果超过了10ms,就给这个g的栈信息上加个标记.
    当有了这个标记,这个g在遇到非内联函数的时候就会把自己中断掉,移到当前p的队列尾.
    
    2. 当协程陷入一个系统调用.没有返回的时候会被调度
    当m进行系统调用的时候,这个m会被陷入到内核态,毫无疑问的被阻塞了.还是sysmon会定期检测,当一个p上的g执行时间过长
    且这个g还是在进行系统调用.那么sysmon会把p与当前m解绑, 然后唤醒一个之前没事做的睡眠的m进行绑定运行.如果没有
    睡眠的m就会重新创建一个m来绑定,之前的m运行着进行系统调用的g返回后就会尝试找没有m的p.如果没有找到,m就会把g放到
    全局队列,然后自己进入睡眠.
    
    3. 当协程被channel阻塞时会被调度
    这个不需要多说.当g被channel阻塞后,这个g会被放到相应的wait队列.该g的状态由_Gruning变成_Gwating,g会被别的g的
    channel操作唤醒,那么这个g就会尝试加入那个g所在的p的runnext,如果没有成功,就会加入本地p队列,全局队列.等待被m运行.
    
    4. 当协程进行网络io阻塞的时候会被调度
    网络io虽然也算是系统调用,但是go原生的网络包底层全是由epoll类似的驱动的,并不走之前系统调用那一套.sysmon线程会进行
    epoll_wait,然后对程序每个socket进行标记,哪些socket是可写的,哪些socket是可读的,然后尝试唤醒因为socket不可写或者
    不可读的g,而g在对socket进行读写的时候就判断sysmon有没有对其标记过,如果没有标记过就进入wait状态,如果标记过了就说明可
    用就继续操作,操作完了,再把这个标记去除了,等待下次标记.
    

    总结:

    1. GMP 三者能够随意绑定解绑,达到了灵活调度的目的
    2. go后台运行这sysmon线程来进行调度.和上帝一样.
    3. go程序底层有g的对象池,已经对g进行了复用不会被销毁. 同时m也是不会被销毁的.所以从某种意义来讲go确实越运行越占内存.
    

    参考资料

    1. https://juejin.im/post/5b7678f451882533110e8948
    2. https://wudaijun.com/2018/01/go-scheduler/
    3. https://www.w3cschool.cn/go_internals/go_internals-8h5b282y.html
    4. ...还有很多很多

    相关文章

      网友评论

          本文标题:自己对go协程的理解

          本文链接:https://www.haomeiwen.com/subject/ladrwctx.html