go并发编程

go语言并发编程

Goroutine所代表的含义:不要用共享内存的方式来通信,应该以通信为手段共享内存。

线程实现模型

  • M : 一个M代表一个内核线程
  • P : 一个P代表M所需要的上下文环境
  • G : 一个G代表一段需要被并发执行的Go语言代码的封装

三者之间的关系:一个G的执行,需要M和P的支持。一个M与P关联之后就形成了一个有效的G运行环境。M与P之间一对一,P与G之间一对多。在三者之上,Go 语言的运行时系统会对这些实体的实例进行分析和调度。

1.M

  • M的创建之初会被加入到全局的M列表中。创建一个M的原因主要是由于没有足够的M来关联P并运行其中可运行的G。除此之外当运行时系统执行监控或者垃圾回收的时候也会导致M的创建。
  • M中最重要的当属如下四个字断
    • curg :正在运行的G指针
    • p:当前M关联的P
    • mstartfn:起始函数
    • nextp:暂存于当前 M 有潜在联系的P(预联)
  • M 创建完成后会先出实话自身的栈空间以及信号等,接下来执行初始函数,若是监控则一直执行,否则执行完毕后会与准备与他关联的P完成关联,完成一个并发环境。
  • M有时会被停止,停止时运行时系统会将其放入调度器的空闲M列表中,注意M是无状态的,气孔线与否以是否存在于调度器的空闲M列表中为依据
  • 单个Go程序所使用的M的最大数量可以被设置,初始值10000,在程序中可以通过runtime/debugs包中的SetMaxThreads来设置M的最大数量。调用时一但给定的新值小于当前M的实际值,则运行时系统会panic。

2.P

  • P是使G能运行在M中的关键,Go 语言的运行时系统会适时的让P与不同的M建立或断开关联,以使得P中的那些可运行的G能在需要的时候活的运行时机。
  • P的数量可以在环境变量GOMAXPROCS被设置,他的最大数量相当于是对可以被并发运行的用户级别的G的数量作出限制。但并不意味着M的数量受到限制。
  • 在确定了最大数量的P后,运行时系统会会根据这个数值初始化全局的P列表。随后会把调度器的可运行G队列中的所有G均匀的放入到全局P列表中的各个P的可运行G队列中。
  • 在运行时系统中同样存在一个调度器的空闲P列表,但P不与M关联后加入其中,需要关联M的时候再取出来。注意在空闲P列表中的可运行G列表不一定是空的。
  • P有状态,如下所示:
    • Pidle: 当前P未与任何M关联
    • Prunning:当前P正在与某个M关联
    • Psyscall:当前P中的被运行的那个G正在进行系统调用
    • Pgcstop:运行时系统正在进行垃圾回收
    • Pdead: 当前P不再被使用
  • 垃圾回收之后P会被置于Pstop状态意味着他们将重新进行调度,在P转化为Pdead状态之前,他的可运行G队列中的G会被转移到调度器的可运行G队列中,而那些被被存放在*G列表中的完成了的G会被转移到调度器的*G列表中。
  • 随着*G列表的不断增大,我们会将其转移至调度器的*G列表中,也会在我们使用Go 语句启用一个G的时候,运行时系统会响应的从啊*G列表中取出一个G来封装我们提供的函数。太少的时候也会将调度器的*G列表中的G转移回来,提高复用率。
  • 当一个P被置于Pdead的时候,*G列表中的G都会别转移到调度器的*G列表中。
  • 在G转入Gdead状态后,首先会放在本地P的*G列表,运行时系统需要用*G封装我们的go函数的时候,也会优先尝试从本地P的*G列表中获取。

3.G

  • 一个G相当于一个Goroutine,也与我们使用Go语句与并罚执行的一个匿名或命名函数相对应。我们编写的go语句会变成一个运行时系统中的函数调用,接到这样的调用后先检查参数的合法性,随后试图从本地的Pd*G列表中和调度器的*G列表中去寻找一个可以用的G。没有则创建。
  • 同样,运行时系统也持有一个G的全局列表,新建的会加入进来,主要作用是存放着所有G的指针。初始化后将G放入本地P的可运行G队列中。供调度器不停的运行各个G。
  • G有状态,如下所示:
    • Gidle:G被创建但还未完全被初始化
    • Grunnable:G可运行并正在等待被运行
    • Grunning:G正在被运行
    • Gsyscall:G正在进行系统调用
    • Gwaiting:G因某个原因而等待
    • Gdead: G运行完成
  • 进入死亡状态的G是可以呗重新初始化并被使用的。相比之下,P在进入死亡状态后只能被销毁
  • G退出系统调用的时候运行时系统会尝试直接运行G,无法直接运行后会将其放入到调度器的*G列表中。从Gsyscall状态和Ggcstop状态转出的G,如果可以立即运行则会被放置于调度器的可运行G队列中。
  • 从Gwaiting中转出来的G,除了因为网络IO而陷入等待的G外,都会被放入到本地的G可运行队列中。
  • 与G有关的非全局容器有调度器的可运行的G队列,调度器的*G列表, 本地P的可运行G队列以及本地P的*G列表。注意这里两个可运行的G队列中的G拥有同等被运行的机会。
    *

调度器

1. 基本结构

  • 五个核心字段:如下所示:
    • gcwaiting:垃圾回收器是否已经开始准备或者正在进行垃圾回收,为1时告诉调度器已经准备执行垃圾回收任务了。
    • stopwait:对还未停止调度的P计数。为0时代表调度工作完全停止。垃圾回收其立即开始执行垃圾回收任务。
    • stopnote:像垃圾回收器告知调度工作已经完全被停止的通知机制的组成部分。
    • sysmonwait:系统监测任务是否已经被暂停的标记
    • sysmonnote:像执行系统监测任务的程序发送通知
  • 调度器的这五个字断都是为了辅助垃圾回收的执行存在的。Go语言垃圾回收的做法是:先停止一切调度工作,包括停止M对P的调度,监控等,然后进行垃圾回收,最后待垃圾回收完成之后再度重启调度工作。
  • 系统监测任务是持续被执行的,系统检测器在检测到gcwaiting的值为1的时候,说明垃圾回收器马上准备工作,此时系统检测器会将调度器的sysmonwait字段的值设置为1以表示系统检测任务已经停止,然后利用sysmonnote字段阻塞自身等待垃圾回收完成,当调度工作重启后,调度器若发现sysmonwait的字段值为1则会利用sysmonnote字段想系统检测器发通知,继续执行检测任务。

2. 一轮调度

具体流程如下图所示:
go并发编程

  • 只要错开了垃圾回收任务的执行时期,调度器就会试图在本地P的可运行G队列中查找可以被运行的G。
  • 在得到一个G后,调度器会让当前M运行他之前判断该G是否已与某个M锁定。若锁定在一起,那么调度器就会让与该G锁定的那个M去运行这个G,然后停止当前M继续等待其他可运行的G。
  • 一轮调度是调度器的核心流程,调度器让某一个G等待之后会进行一轮调度,又比如,在垃圾回收结束时也会进行一轮调度,再比如当某个G退出系统调用的时候也会启动一轮调度的流程。

3. 全力查找可运行的G

  • 全力查找G的子流程如下所示:
    1. 从本地P的可运行队列中获取G
    2. 从调度器的可运行队列中获取G
    3. 从网络I/O轮询器处查找已经就绪的G
    4. 在条件许可的情况下从另一个P的可运行队列中偷取可运行的G
    5. 再次尝试从调度器的可运行队列中获取G
    6. 尝试从所有P的可运行队列中获取G
    7. 再次尝试从网络IO轮询器处查找就绪的G
  • 第三部的具体实现逻辑是指当一个goroutine试图在网络连接上进行读写操作的时候,底层程序会让网络IO轮询器在他们准备好之后通知改goroutine,将其*转入等待状态(Gwaiting),然后调度器会使它与运行它的那个M分离。
  • 如果查找之后仍然没有结果,调度器会停止当前的M,等待出现可执行G时再唤醒。如果一直找不到,查找G的子流程将一直进行下去。
  • 所以全力查找可运行G的子流程结束意味着当前的M抢到一个可运行的G。

4. 启用或停止M

  • 在调度器停止某个M之前一定会把它放入到自己的空闲M列表中,而调度器准备唤醒的M一定是从它的空闲M中取出来的。
  • 停止当前M总体有两个步骤,首先是将当前M放入到空闲M列表中,然后阻塞它以等待其他M中的运行的调度程序发现多个可运行的G。

启用或停止M的流程如下图所示:
go并发编程

  • (M2)当一个M被锁定时,如果想要停止当前被锁定的M,则调度程序首先会断开与该被锁定M的P,使之与其他的M关联以保证不浪费上下文环境,随后,被锁定的M会被停止进入等待状态,等待运行与之锁定的G。
  • (M6)当M6中的调度程序发现它找到的可运行的G已经与某个M锁定了(M2),他就会进入到启用被锁定的M的流程中。
    • 调度程序会先获取与该可运行G锁定的那个M(M6)
    • 然后断开与该M(M6)关联的P
    • 把该P与这个被锁定的M(M2)预联。这既是为了拿掉当前M(M6)的上下文环境,也是为了预设被锁定的M(M2)的上下文环境,保证在被锁定的M(M2)被唤醒后调度程序将曾经的M(M6)的P关联在一起,做到不同M程序共同完成对上下文环境的转移。
    • 之后M就可以运行与之锁定的G了

5. 系统监测任务

流程图如下图所示:
go并发编程
系统检测主要做了以下三件事,

  1. 必要的时候从网络IO轮询器处查找已经就绪的G并经他们放入到调度器的可运行G队列中。
  2. 抢夺符合条件的P和G,流程图如下所示:
    go并发编程
  3. 在必要时进行调度器跟踪并打印信息

6. 变更P的最大数量

流程图如下:
go并发编程