Go的并发机制

Go的并发机制

go的线程实现由3种模型,有3个核心元素:
M:machine 一个M代表一个内核线程,或者说工作线程
P:Process 一个P代表执行Go代码所需要的必须的资源,或称为 上下文环境
G:goroutine 一个G代表一个go代码片段,是对go代码片段的一个封装

一个G需要M和P的支持
M结构体的字段说明:

mstartfn : 表示M的起始函数,其实就是在编写go语句时,指定的那个go func
curg:会存放当前M正在运行的那个g的指针
p:指向与当前指针关联的那个P
nextp:用来指向与当前M有潜在关联的P
调度器将某个P赋值给M的nextp字段,称为对M和P的预关联
spinning:是bool类型的,用来表示这个M是否正在寻找可运行的G,寻找的过程,M会处于自旋状态
Go系统运行时,会把一个G和一个M锁定在一起,这样,这个G只能有这个M来运行,这个M也只能运行这个G
标准代码库runtime中的函数LockOSThread和UnLockOSThread也为我们提供了锁定和解锁的方法
lockedg字段提供的是与当前M锁定的G,如果有的话

M在创建之初就会被假如到全局的M列表(runtime.allm)中,这时它的起始函数和预关联的P也会被设置,最后运行时系统会为这个M创建一个新的内核线程并与之相关联,M就为G的执行做好了准备
运行时系统在停止M的时候会把它放到runtime.sched.midle中(重要),因为在需要一个新的未被使用的M时,运行时系统会首先从这个列表中获取,M是否空闲,仅以它是否存在于线程的空闲列表中为依据,单个GO程序所使用的最大的M数量是可以设置的,Go程序运行时会先启动一个引导程序,这个程序为其建立必要的环境

P是G能够在M中运行的关键,Go的运行时系统会适时的让P与不同的M建立或断开关联,以使P中那些可运行的G能够及时获得时机,P的数量实际上是对程序中并发运行的G的一种限制,P的数量就是可运行G的队列的数量,一个G在启用后会先被追加到某个P的可运行G队列中,以等待运行时机,-个P只有与M关联后才会使其可运行G队列中的G有机会运行,不过,设置P的最大数量只能限制住P的数量,对M和G的数量不能约束,当M因系统调用而阻塞(或者说,它运行的G进程陷入了系统调用的时候),运行时系统会把该M和与之关联的P分开,这时如果,这个P的可运行G队列中还有未被运行的G,那么运行时系统就会找到一个空闲的M或者创建一个M,M的数量也因此一般会多于P,而G的数量一般取决于Go程序本身

Note:Go未对何时调用runtime.GOMAXPROCS做出限制,但它的执行会暂时让所有的P都脱离运行状态,并试图阻止任何用户级别的G,直到设定完成才恢复执行,会有很大的性能损耗,所以,最好放在main程序之前调用

在确定P的最大数值之后,运行时系统会根据这个数值重整全局的P列表(runtime.allp),与全局
M类似,包含全部运行时系统创建的p,运行时系统会把这些P中的可运行的G全部取出,并放入调度器的可运行G队列中,那些被转移的G,会在以后经由调度再次放入到某个P的可运行G

与M不同,P本身是具有状态的,可能的状态如下:
Pidle:表明当前P未与任何M存在关联
Prunning:正在与某个M关联
Psyscall:当前P中运行的那个G正在进行系统调用
Pgcstop:运行时系统需要停止调度,eg 运行时系统开始进行垃圾回收之前会试图把全局P列表的所有P都置于此状态
Pdead:当前P不会再被使用,在Go程序运行的过程中,调用runtime.GOMAXPROCS减少当前P的最大数量,多余的P就会处于这个状态

P在创建之初的状态是Pgcstop,但并不意味着运行时系统要进行垃圾回收,这个时间也会非常短暂,紧接着的初始化之后,状态会变为Pidle

Pgcstop->Pidle->Prunning->Psyscall(暂停调度,恢复则统一转为idle状态,处于统一起点)->dead(停止调度)

非Pcstop状态的P都可能因为全局P列表的缩小被认为是多余的,而被置于Pdead状态,其中的可运行G队列会被转移到调度器的可运行G队列,自由G列表会被转移到调度器的自由G列表

一个G代表一个Goroutine(Go例程),也与Go函数对应,Go的编译器会把go语句变成对内部函数newproc的调用,并把go函数及参数都传递给这个函数

在特定的情况下,一旦新启用的G倍存于某地,调度就会立即进行以使该G尽早运行,不过,这里不立即调度也无需担心,因为运行时系统总在为每个G忙碌着

G的主要状态:
Gidle:当前G刚被分配,但还未被初始化
Grunnable:当前G正在可运行队列中等待运行
Grunning:当前G正在运行
Gsyscall:当前G正在执行某个系统调用
Gwaiting:当前G正在被阻塞
Gdead:当前G正在闲置
Gcopystack:当前G的栈正在被移动,有可能是栈的收缩或者扩展

Note:Gdead状态的G会被放入到本地P或者调度器的自由G列表,可以重新被初始化并使用

调度器的基本结构
gcwaiting:uint32是否需要因一些任务而停止调度
stopwait:int32 需要停止仍未停止的P的数量
stopnote:note 用于实现与stopwait相关的事件通知机制
sysmonnote:note 实现与sysmonwait相关的事件通知机制
sysmonwait:停止调度期间系统监控任务是否在等待

gcwaiting用于表示是否需要停止调度,在停止调度前,值被设置为1,恢复调度之前设为0,一些调度任务只要发现值为1,就把当前P的状态设置为gcstop,将stopwait的值-1,若减后值为0,说明所有P的状态都是gcstop了,就可以利用stopnote字段唤醒因等待调度停止而暂停的串行运行时任务了

sysmonwait和wywmonnote的作用与前边类似,只不过针对的是系统监测任务

一次内核调度:
如果调度器找到一个M,但发现它与某一个G锁定,就会立即停止调度,并停止当前M(阻塞,等待锁定的G来唤醒),一旦它锁定的G处于可运行状态就会被唤醒,继续运行,停止M意味着相关的内核线程无法再做其他的事了,造成浪费计算资源

相应的,若调度器为当前M找到一个可运行的G,但发现它与某个M锁定,它就会唤醒那个M以运行该G,并继续为当前M寻找可以运行的G

如果调度器发现当前M未与任何G锁定,调度就会继续执行,检测是否有运行时串行任务在等待(它的执行需要暂停Go调度器),如果gcwaiting的值为1,调度器会停止并阻塞当前M以等待运行时穿行任务的执行完成,一旦串行任务执行完成,它会被唤醒

若M未被锁定,且无串行时任务,调度器就开始为它寻找可运行的G

Go调度器并不是运行在某个专用线程中的程序,它会运行在若干已存在的M(或者说内核线程)之中,换句话说,运行时系统几乎所有的M都会参与调度任务的执行,它们共同实现了Go调度器的调度功能

全力查找可运行的G:

1. 获取执行终结器的G,垃圾回收器在回收对象之前会限制性与之关联的终结函数(如果有的话)所有的终结函数都有一个专门的G负责,调度器在判断这些专用G已完成任务之后试图获取它,置为Grunnable状态,放入本地可运行P列表
2. 从本地P的可运行G队列获取G
3. 从调度器的可运行G队列获取G
4. 从网络IO轮询器(netpoller)处获取G,若netpoller已被初始化,且有过网络IO操作,调度器会试着从netpoller那里获取一个G列表,把作为表头的那个G作为结果返回,其余的G放入调度器的可运行G队列
5. 从其它P的可运行G队列获取
6. 获取执行GC标记任务的G,搜索的第二阶段,调度器会先判断是否处于GC标记阶段,本地P是否可用于GC标记任务,都为真的话,调度器会把本地P持有的GC标记专用G置为Grunable状态,并作为结果返回
7. 从调度器的可运行G队列获取,调度器再次尝试从该处获取一个G,依然找不到则解除本地P与当前M的关联,并把该P放入调度器空闲P列表
8. 从全局P列表中每个P的可运行G队列获取G,遍历全局P列表的P,并检查它们的可运行G队列是不是空,否的话就从调度器的空闲P列表取出一个P,判断可用后与当前M关联在一起
9. 获取执行GC标记任务的G
10. 从网络IO轮询器中获取G,与4)基本相同,但它是阻塞的,直到有可用的G,阻塞才会被解除

GO进行GC的三种模式:

1. gcbackgroundMode:并发的执行垃圾收集(标记)和清扫
2. gcForceMode:串行的执行垃圾收集(执行时停止调度),但并发的执行清扫
3. gcForceBlockMode:串行的执行垃圾的收集和清扫

调度器驱使的自动GC和系统监控任务中的强制GC都会以gcBackgroundMode的模式执行
但是前者会检测Go程序当前的内存使用量,仅当使用增量过大时才真正执行GC,后者无视这个条件

Go调度器如何管理goroutine

当正在运行的goroutine需要做一个阻塞的系统调用,eg打开一个文件的时候,线程和goroutine会从逻辑处理器上分离,该线程会继续阻塞,等待系统调用返回,与此同时,逻辑处理器失去了用来运行的线程,所以,调度器会创建一个新的线程,并绑定到该逻辑处理器,之后,调度器会从本地运行队列选择另一个goroutine来运行,当被阻塞的系统调用执行完成并返回时,对应的goroutine会放回本地运行队列,之前的线程会被保存,以便下次调用

如果一个goroutine需要做一个网络IO调用,goroutine会和逻辑处理器分离,转移到网络轮询器的运行时队列,一旦该轮询器指示某个网络的读写操作已经就绪,对应的goroutine就会重新分配到逻辑处理器上来完成操作,调度器对可以创建的逻辑处理器的数量没有限制,但语言默认限制的线程数为10000个,可以调用runtime/debug包的SetMaxThreads方法修改,但是如果程序试图使用更多的线程,有可能会导致系统崩溃

并发:是同时管理很多事情
并行:是让不同的代码段同时在不同的物理处理器上执行,并行的关键是可以同时做很多事情
并发的效果比并行好,因为操作系统和硬件的总资源一般很少,但支持操作系统同时做很多的事情

竞争状态: 如果2个或者多个goroutine在相互没有同步的情况系,访问某个资源并试图同时读写这个资源,就处于相互竞争的状态,这种情况就称为

使用原子和互斥锁都可以工作,但使用它们捕获让编写并发程序变得简单,更不容易出错或者是更有趣,在go的世界里,还有通道,可以使用channel通过发送和接收需要共享的资源>,在goroutine之间做同步

当一个资源需要在goroutine之间共享时,通道在goroutine之间架起了一个管道,并提供了确保同步交换数据的机制,声明通道时需要指定将要被共享的数据的类型,可以通过通道来共享内置类型、命名类型、结构类型和引用类型的值或者指针

你可能感兴趣的:(go)