Golang并发模型
前言
Go实现了两种并发形式。第一种是大家普遍认知的:多线程共享内存,这是最常见的各语言中的多线程并发模型,go本身也支持。另外一种是Go语言特有的,也是Go语言推荐的CSP(communicating sequential processes)并发模型。Go语言提倡以通信的方式来共享内存,这句话相信看过一些go相关文档的同学一定不陌生:
Do not communicate by sharing memory; instead, share memory by communicating.
那么本篇就来看一看go所推崇的并发模型
线程模型
在讲并发模型之前,首先来看看线程的模型。
我们知道,linux线程的工作区域被划分为用户空间和内核空间,用户空间是程序内部活动所处在的空间,当涉及到资源调用时(CPU/IO/MEM/NET等资源),需要向内核发起内核syscall,由内核来完成对程序的资源分配。因此,程序的线程(用户线程),最终都是落到内核线程上来被执行的,内核态的线程,简称KSE
,一般一个内核线程对应一颗cpu核心。用户线程和KSE线程的关联映射模型分为以下几种:
用户级线程模型
如图所示,进程内的多个用户态的线程对应着一个内核线程,程序线程的创建、终止、切换或者同步等线程工作必须自身来完成。
内核级线程模型
这种模型直接调用操作系统的内核级线程,所有线程的创建、终止、切换、同步等操作,都由内核来管理。
两级线程模型
这种模型是介于用户级线程模型和内核级线程模型之间的一种线程模型。这种模型的实现非常复杂,和内核级线程模型类似,一个进程中可以对应多个内核级线程,但是进程中的线程不和内核线程一一对应;这种线程模型会先创建多个内核级线程,然后用自身的用户级线程去对应创建的多个内核级线程,自身的用户级线程需要本身程序去调度,内核级的线程交给操作系统内核去调度。
Go语言的goroutine
线程模型就是一种特殊的两级线程模型,即我们常说的MPG模型。
传统并发模型
普通的线程并发模型,就是像Java、C++、或者Python,线程间通信都是通过共享内存的方式来进行的。非常典型的方式就是,在访问共享数据(例如数组、Map、或者某个结构体或对象)的时候,通过锁来访问这些数据结构,然后基于里面的数据块,并发执行相关操作。因此,在很多时候,衍生出一种方便操作的数据结构,叫做“线程安全的数据结构”。例如Java提供的包”java.util.concurrent”中的数据结构。Go的sync包也提供了对传统的线程并发模型的支持,但Go本身更推崇CSP的并发模型,通过goroutine和
channel`来组合实现:
goroutine
是Go语言中并发的执行单位。和传统概念上的”线程“类似,可以理解为go语言的微线程channel
是Go语言中各个并发执行单位(goroutine
)之间的通信机制。 通俗的讲,就是各个goroutine
之间通信的”管道“,类似于Linux中的pipe管道
关于go的传统并发模型、channel相关说明,这里不作赘述,直接进入go CSP(MPG)并发模型
MPG模型
M
指的是Machine
,一个M
直接关联了一个内核线程,可以直接理解为M就是内核线程。P
指的是processor
,代表了M
所需的上下文环境,也是处理用户级代码逻辑的处理器。G
指的是Goroutine
,即Go实现的轻量级线程。
MPG关系图:
以上这个图讲的是两个线程(内核线程)的情况。一个M会对应一个内核线程,一个M也会连接一个上下文P,一个上下文P相当于一个“处理器”,一个P连接一个或者多个Goroutine。简单来说,G是go实现的微线程,G要想进入到M中执行,需要通过P来协调。P的数量是在启动时被设置为环境变量GOMAXPROCS的值,或者通过运行时调用函数runtime.GOMAXPROCS()
进行设置。Processor数量固定意味着任意时刻只有固定数量的线程在运行go代码。Goroutine中就是我们要执行并发的代码。如图所示,P正在执行的Goroutine
为蓝色的;处于待执行状态的Goroutine
为灰色的,灰色的Goroutine
形成了一个队列runqueues
。
这个模型多引入了一个P的角色,作为中间人来协调goroutine和内核线程的调度分配,下面着重理解一下P这个角色的作用。
P的作用
1.保存上下文
P起着保存Goroutine上下文的作用,例如线程在执行syscall时,线程G和实际执行的线程M会进入阻塞状态,则此时P需要保存好上下文状态,然后被其他的M内核线程所接管,以便开始调度P管理的其他G线程。
如图所示:
G0–>M0在执行syscall后阻塞了,则P会被M1接管,以便调度runqueues内的其他G线程。
2.均衡分配G线程
当某个P所接管的G队列为空时,代表当前队列里已经没有需要调度的线程了,但别的P管理的队列中或许还有任务在排队,因此,Go的做法是,空闲的P会从繁忙的P的队列中抽取一半的G到自身的队列中来,以均衡分配工作线程。
总结
MPG模型中,P这个角色容易使人迷惑,理解它的作用有助于理解MPG模型的整体工作模式。