一 go通信主张

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

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

Go的著名主张:

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

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

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

二 Go线程模型

2.0 线程模型三元素

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

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

每个G的执行需要P和M的支持,M与P关联后才会形成一个有效的G运行环境,即 工作线程+上下文环境

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

3.1 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,该值越早设定越好。

3.2 P

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

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

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

贴士:

P的数量即是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的生命,如图:

贴士:

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

3.3 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都会由运行时根据需要设置为不同的状态:

  • Gidle:刚被分配,未初始化
  • Grunnabel:正在可运行队列中等待运行
  • Grunning:正在运行
  • Gsyscall:正在执行某个系统调用
  • Gwaiting:G被阻塞中
  • Gdead:G闲置中
  • Gcopystack:G的栈因为扩展或收缩,正在被移动

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

  • Gscanrunnable:G等待运行中,它的栈也被正在扫描(因为垃圾回收)
  • Gscanrunning:G运行中,它的栈正在被GC扫描

G的状态转换图:

注意:

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

四 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队列中

results matching ""

    No results matching ""