GO的线程模型

go语言本身是为并发而打造的语言。那它的线程模型是怎么样的呢?

传统是怎么处理是这样的,把数据放到共享内存,给多线程使用,这个方式是不是看上去非常简单,但是在并发访问的控制上就变的很复杂。

go语言的是这么处理的,它不推荐用共享内存的方式传递数据,它推荐使用channel。channel主要用来在多个go语句片段之前传递数据,这样做还会保证并发的安全性。不过go还是保留了传统的方法(互斥量、条件变量等)。

了解go的线程模型之前,我们需要知道go的核心元素,它们支撑起了这个模型的主框架。

  1. M(machine)。一个M对应一个内核线程。
  2. P(processor)。一个P代表执行一个Go代码片段必需的资源。
  3. G(goroutine)。一个G代表Go代码片段。

 

简单说说这3个元素的关系:

  1. 1个G的执行需要一个P和M的支持。
  2. 1个M在与一个P关联之后就形成了一个G的运行环境。
  3. 每个P都会包含一个可运行的G的队列。(这个队列里的G会被依次传递给与P关联的M,并获得运行时机)

M:

M结构的字段不止这些,这里就只挑几个重要的字段:

  1. g0:表示一个特殊的goroutine。这个goroutine是系统运行时创建的,用于执行一些运行任务。
  2. mstartfn:表示M的起始方法。这个函数其实是我们在编写go语句时携带的方法
  3. curg:表示当前正在执行的G的指针
  4. p:表示当前M关联的P。
  5. nextp:表示与当前M有潜在关联的M,这种关联也可以叫预联。(运行时系统有时候会把刚刚重新启动的M和已与它预联P关联在一起。)
  6. spinning:表示当前M是否正在寻找可运行的G(寻找动作称为自旋状态)。
  7. lockedg:表示当前M锁定的G的指针(当锁定后,当前M只能运行锁定的G,锁定的G也只能由这个M也运行,go的标准库代码包中的runtime.LockOSThread和runtime.UnlockOSThread方法提供了锁定解锁方法)。

当M被创建开始的时候会被加入全局M中,这时的起始方法和预联也会被设置。然后系统会为M专门创建一个线程与之对应。这里全局M主要是用来 运行时系统在需要的时候,会通过它获取到所有的M信息,也可以防止被垃圾回收。

当M被创建后,Go运行时系统会先对它进行一系列的初始化,初始化包含了它所有的栈空间以及信号处理。初始化完成后,如果它有起始方法的话,并执行(如果这里的起始方法是系统监控任务的话,当前M会一直执行,不会停止)。当执行完毕后,当前M会与预联P完成关联,并准备执行预联P所关联的G。当执行完,M会继续寻找可运行的G并运行。这一过程也是调度的一部分。

全局M有时候也是会被停止,比如在运行时系统执行垃圾回收任务的过程中。运行时系统会把M停止并放入到调试器的空闲M列表中。当一个M未被使用的时候,运行时系统会先从这个列表中获取,M是否空闲,就是判断它是否在空闲M列表中。

go程序使用的M默认最大是10000个。也就是说一个go程序最多可以使用10000个M,这个10000个M也是可以设置的。可以通过runtime/debug.SetMaxThreads方法。但是如果在程序中通过设置最大的M的时候,给定的数量小于实际数量,运行时系统会报一个恐慌。所以尽量越早调越好。

P:

P是G能够在M中运行的关键。Go在运行时,会让P与不同的M进行关联或断开。使P中的可运行G能够获取运行时机,这个类似操作系统内核在CPU之上实时的切换不同的进程或线程。

改变P的最大数量也是有方法的,有2种方法。

  1. runtime.GOMAXPROCS并把要设置的数量传入。(调用runteim.GOMAXPROCS方法,会暂时让所有的P都脱离运行状态,并试图阻止用户G的运行,直到设置完后恢复,这个过程也是会有性能的消耗,可以尽量在main方法开始调用)
  2. 设置环境变量GOMAXPROCS的值。

注意:设置最大数量P的时候,如果传入大于0的值,系统就认为是有效的,如果传入值大于P的上限值(256,这个上限值在GO后续更新的时候可能会变化)则,系统会用上限值代替。

其实对P的最大数量设置也是对程序的并发运行G的规模一种限制。一个P的数量相当于可运行G队列的数量。因为一个G在被启用后,会被加到某一个P的可运行G队列中。当一个P与一个M关联在一起的时候,P的可运行G队列中的G才有机会运行。

不过,这个可运行G队列中的G也是会被转移的,当与这个P关联的M因syscall阻塞的时候(这里阻塞指的是P关联的G),这个P会与M分离。如果这个时候P中的可运行G队列中还有G的话。运行时系统会获取一个空闲M或创建一个M与之关联,并运行可运行G队列中的G。

P在行动中的状态:

  1. Pidle:表示当前P未与任何M有所关联
  2. Prunning:表示当前P与某一个M有所关联
  3. Psyscall:表示当前所关联的G正在系统调用。
  4. Pgcstop:表示运行时系统需要停止调度当前P。(比如运行时候系统在开始垃圾回收的某些步骤前,就会试图把全局P列表中的所有P都设置为此状态)
  5. Pdead:表示当前P已经不会再被使用,可以销毁。(如果设置了最大数量,这个数量比之前的小的话,多出来的P会被设置为这个状态)

P状态的转换:

G:

一个G代表一个goroutine(或称Go例程),也与go方法相对应,在我们编程的时候,只是使用go语句向Go的运行时候系统提交一个并发任务,然后Go的运行时候系统会并发的执行它。

Go的编译器会在编译的时候把go语句变成内部方法newproc调用,并把go方法及参数都作为参数传递给这个方法。

当运行时候系统接收到这个方法后,会先检查go函数和参数是否合法,之后会尝试从当前P(当前M所关联的P)的自由G队列或调度器的自由G队列获取一个可用的G,如果没获取到,会创建一个G,并添加到全局G队列中,运行时系统会对每一个G进行初始化,包含关联的go函数、G的状态、和ID等,然后创建G完后,这个G就会被与当前P所关联,会设置到P的runnext字段(存放最新的G,提醒这个G优先执行)中,如果这个字段中已有G,则这个旧G会被放到当前P的可运行队列的底部,如果这个队列已满,会被放到调度器的可运行队列底部。

G在行动中的状态:

  1. Gidle:表示刚创建,还未初始化。
  2. Grunnable:表示可运行(可运行队列中),正在等待运行。
  3. Grunning:表示正在运行。
  4. Gsyscall:表示正在系统调用。
  5. Gwaiting:表示正阻塞。
  6. Gdead:表示正在空闲。
  7. Gcopystack:表示当前G的栈正在移动。
  8. Gscan:此状态不能单用,于组合使用。(比如与runnable组合:表示待运行的G的栈正在被扫描,原因有很多,比如被GC任务执行。)

G在退出系统调用时的状态转换要比上述复杂一些。运行时系统会先尝试直接运行这个G,如果无法直接运行,才会把它转换为runnable状态并放入调度器的自由G列表中。然后在调度过种中在次被运行。

G的dead状态后还是可以重新初始化被使用,而P的dead之后就只能结束摧毁。所以G和P的dead状态代表的含义是不一样的。另外,dead状态的G会被放入本地P或调度器的自由G队列,这是它们被重用的前提。

 

 

 

 

(有有趣的小伙伴可以一直看go并发编程实战,另如有哪里不对,请联系博主,感激不尽~!)


参考文献:

[ 1 ] 郝林.GO并发编程实战

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值