go语言本身是为并发而打造的语言。那它的线程模型是怎么样的呢?
传统是怎么处理是这样的,把数据放到共享内存,给多线程使用,这个方式是不是看上去非常简单,但是在并发访问的控制上就变的很复杂。
go语言的是这么处理的,它不推荐用共享内存的方式传递数据,它推荐使用channel。channel主要用来在多个go语句片段之前传递数据,这样做还会保证并发的安全性。不过go还是保留了传统的方法(互斥量、条件变量等)。
了解go的线程模型之前,我们需要知道go的核心元素,它们支撑起了这个模型的主框架。
- M(machine)。一个M对应一个内核线程。
- P(processor)。一个P代表执行一个Go代码片段必需的资源。
- G(goroutine)。一个G代表Go代码片段。
简单说说这3个元素的关系:
- 1个G的执行需要一个P和M的支持。
- 1个M在与一个P关联之后就形成了一个G的运行环境。
- 每个P都会包含一个可运行的G的队列。(这个队列里的G会被依次传递给与P关联的M,并获得运行时机)
M:
M结构的字段不止这些,这里就只挑几个重要的字段:
- g0:表示一个特殊的goroutine。这个goroutine是系统运行时创建的,用于执行一些运行任务。
- mstartfn:表示M的起始方法。这个函数其实是我们在编写go语句时携带的方法
- curg:表示当前正在执行的G的指针
- p:表示当前M关联的P。
- nextp:表示与当前M有潜在关联的M,这种关联也可以叫预联。(运行时系统有时候会把刚刚重新启动的M和已与它预联P关联在一起。)
- spinning:表示当前M是否正在寻找可运行的G(寻找动作称为自旋状态)。
- 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种方法。
- runtime.GOMAXPROCS并把要设置的数量传入。(调用runteim.GOMAXPROCS方法,会暂时让所有的P都脱离运行状态,并试图阻止用户G的运行,直到设置完后恢复,这个过程也是会有性能的消耗,可以尽量在main方法开始调用)
- 设置环境变量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在行动中的状态:
- Pidle:表示当前P未与任何M有所关联
- Prunning:表示当前P与某一个M有所关联
- Psyscall:表示当前所关联的G正在系统调用。
- Pgcstop:表示运行时系统需要停止调度当前P。(比如运行时候系统在开始垃圾回收的某些步骤前,就会试图把全局P列表中的所有P都设置为此状态)
- 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在行动中的状态:
- Gidle:表示刚创建,还未初始化。
- Grunnable:表示可运行(可运行队列中),正在等待运行。
- Grunning:表示正在运行。
- Gsyscall:表示正在系统调用。
- Gwaiting:表示正阻塞。
- Gdead:表示正在空闲。
- Gcopystack:表示当前G的栈正在移动。
- Gscan:此状态不能单用,于组合使用。(比如与runnable组合:表示待运行的G的栈正在被扫描,原因有很多,比如被GC任务执行。)
G在退出系统调用时的状态转换要比上述复杂一些。运行时系统会先尝试直接运行这个G,如果无法直接运行,才会把它转换为runnable状态并放入调度器的自由G列表中。然后在调度过种中在次被运行。
G的dead状态后还是可以重新初始化被使用,而P的dead之后就只能结束摧毁。所以G和P的dead状态代表的含义是不一样的。另外,dead状态的G会被放入本地P或调度器的自由G队列,这是它们被重用的前提。
(有有趣的小伙伴可以一直看go并发编程实战,另如有哪里不对,请联系博主,感激不尽~!)
参考文献:
[ 1 ] 郝林.GO并发编程实战