在这里插入图片描述

1、go通信主张

数据放在共享内存中提供给多个线程访问的方式,虽然思想上简单,但却有两个问题:

  • 使并发访问控制变得复杂
  • 一些同步方法的使用会让多核CPU的优势难以发挥

Go的著名主张:

不要用共享内存的方式来通信,应该以通信作为手段来共享内存

Go推荐使用通道(channel)的方式解决数据传递问题,在多个goroutine之间,channel复杂传递数据,还能保证整个过程的并发安全性。

当然Go也依然提供了传统的同步方法,如互斥量,条件变量等。

2、Go线程模型

2.1 线程模型三元素

Go的线程实现模型有三个元素,即MPG:

  • M:machine,一个M代表一个工作线程
  • P:processor,一个P代表执行一个Go代码段需要的上下文环境
  • G:goroutine,一个G代表一个Go协程
工作线程+上下文环境

内核调度实体(KSE)负责调度这些工作线程M,每个实体对应一个M,如图所示:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-738SUSlb-1606054529972)(../images/go/02-05.svg)]

2.2 M

工作线程M用来关联上下文环境P。

创建新的工作线程M时机:

  • 没有足够的工作线程
  • 一些特殊情况下,如:执行系统监控,执行垃圾回收等

M常用源码字段如下:

type m struct {
    g0 *g                   // Go运行时启动之初创建,用于执行运行时任务
    mstartfn func()         // M起始函数,即代码中 go 携带的函数
    curg *g                 // 存储当前正在运行的代码段G的指针
    p puintptr              // 指针:当前关联的上下文P
    nextp puintptr          // 指针:与当前M有潜在关联的P,调度器将某个P赋给某个M的nextp,则及时预关联
    spinning bool           // 当前M是否正在寻找可运行的G
    lockedg *g              // 与当前M锁定的G
}

M的生命:

  • 创建:
    • M被创建后,就会加入全局M列表(runtime.allm),并设定M的 mstartfn、p (起始函数、上下文环境)
    • 然后,运行时为M创建一个新工作线程并与之关联
    • 起始函数只作为系统监控和垃圾回收使用(通过起始函数可以获取M所有信息,也可以防止M被当做垃圾回收掉)。
  • 停止:
    • 运行时停止M时,M会被放入空闲M列表(runtime.sched.midle)
    • 运行时从该列表中回收M

Go可以手动设定可以使用的M数量:

runtime/debug.SetMaxThreads

一个Go程序默认最多可使用10000个M,该值越早设定越好。

2.3 P

goroutine(即G)如果需要运行,需要获得运行时机。当Go运行时让上下文环境P与工作线程M建立连接后,P中的G就可以运行。

P的结构包含两个重要成员:

  • 可运行G队列:要运行的G列表
  • 自由G列表:已完成运行的G列表,可以提高G的复用率

贴士:

runtime.GOMAXPROCS

P的重复利用:

  • 连接:Go运行时让P与M连接后,P中的G开始运行
  • 分离:G进入系统调用后,运行时会将M和对应的P分离
    • 如果P的可运行队列中还有未被运行的G,运行时会找到一个空闲的M或者创建新的M,并与该P关联,以满足剩余的G运行需要,所以一般情况下M的数量都会比P多。
  • 空闲:P与M分离后,会被放入空闲P列表(runtime.sched.pidle)
    • 此时会清空P中的可运行G队列,如果运行时需要一个空闲的P与M关联,则从该列表取出一个

P的生命,如图:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-tbbSH1Jt-1606054529974)(../images/go/02-06.svg)]

贴士:

  • 非Pdead状态的P都会在运行时要停止调度时被设置为Pgcstop状态,等到要重新调度时,不会被恢复到原有状态,而是统一被转换为Pidle状态,公平接受再次调度。
  • 自由G列表会随着完成运行的G增多而增大,到一定程度后,运行时会将其中部分G转移到调度器自己的自由G列表中。

2.4 G

一个G代表一个Go协程goroutine,即go函数。

在go函数启动一个G时:

  • 运行时会先从相应的P的自由G列表获取一个G封装go函数
  • 如果P的自由G列表为空,则会从调度器本身的自由G列表中转移过来一些G到P的自由G列表中
  • 如果调度器本身的自由G列表也为空,则新建一个G

运行时本身持有一个G的全局列表(runtime.allgs),用于存放当前运行时系统中所有G的指针,新建的G会被加入该列表。

执行步骤:

  • 初始化:无论是新G还是取出来的G都会被运行时初始化,包括其关联函数、G状态、ID
  • 将初始化后的G存储到本地P的runnext字段中
  • 如果runnext字段中已经存在G,则存在的G会被踢到该P可运行G队列的末尾,如果队列已满,则G只能追加到调度器本身的可运行G队列中

每个G都会由运行时根据需要设置为不同的状态:

GrunnabelGrunningGsyscallGwaitingGdeadGcopystack

G还有一些组合状态Gscan,组合态代表G的栈正在被GC扫描,如:

GscanrunnableGscanrunning

G的状态转换图:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-tNbrn35K-1606054529976)(../images/go/02-07.svg)]

注意:

  • 进入死亡状态Gdead的G可以重新被初始化
  • 但是P进入死亡状态Pdead后只能被销毁
3、MPG容器

MPG常见容器:

名称源码作用域说明
全局M列表runtime.allm运行时存放所有M的单向链表
全局P列表runtime.allp运行时存放所有P的数组
全局G列表runtime.allgs运行时存放所有G的切片
空闲M列表runtime.sched.midle调度器存放空闲M的单向链表
空闲P列表runtime.sched.pidle调度器存放空闲P的单向链表
调度器可运行G队列runtime.sched.runqhead runtime.sched.runqtail调度器存放可运行的G的队列
调度器自由G列表runtime.sched.gfreeStack runtime.sched.gfreeNoStack调度器存放自由G的两个单向链表
P可运行G队列runtime.p.runq本地P存放当前P中可运行的G的队列
P自由G列表runtime.p.gfree本地P存放当前P的自由G的单向链表

贴士:

  • 任何G都会存在于全局G列表中,其余的4个容器则只会存放在当前作用域内的具有每个状态的G。
  • 调度器的可运行G列表和P的可运行G列表拥有几乎平等的运行机会:
    • 刚被初始化的G都会被放入本地P的可运行G队列
    • 从Gsyscall状态转出的G都会被放入调度器的可运行G队列
    • Gdead状态的G,会被放入本地P的自由G列表

两个可运行G队列会互相转移G:

  • 调用runtime.GOMAXPROCS函数,会导致运行时系统把将死的P的可运行G队列中的G全部转移到调度器的可运行G队列
  • 如果本地P的可运行G队列已满,则一半的G会被转移到调度器可运行G队列中
    在这里插入图片描述