请记住go并发的真理:
Do not communicate by sharing memory; instead, share memory by communicating.
不要以共享内存的方式来通信,相反,要通过通信来共享内存。
go语言天生的并发大家都知道,可是go是如何实现轻松的构造上万的协程呢?今天我们就说一下go并发的MPG模型。
- M 代表着一个内核线程,也可以称为一个工作线程。goroutine就是跑在M之上的
- P 代表着(Processor)处理器 它的主要用途就是用来执行goroutine的,一个P代表执行一个Go代码片段的基础(可以理解为上下文环境),所以它也维护了一个可运行的goroutine队列,和自由的goroutine队列,里面存储了所有需要它来执行的goroutine。
- G 代表着goroutine 实际的数据结构(就是你封装的那个方法),并维护者goroutine 需要的栈、程序计数器以及它所在的M等信息。
- Seched 代表着一个调度器 它维护有存储空闲的M队列和空闲的P队列,可运行的G队列,自由的G队列以及调度器的一些状态信息等。
多个goroutine并发合作
上面这个图生动的说明多个协程工作形式,其中每一个gopher(土拨鼠)可以看作一个协程(G),其实对于这些gopher,还有一个包工头的gopher,他来管理这些工作的gopher,这个包工头就可以看作一个 Schedule调度器。
MPG
我们在看上面这个图,图中P正在执行的Goroutine为蓝色的,处于待执行状态的Goroutine为灰色的,灰色的Goroutine形成了一个队列run queues(本地队列)。
我们再看一下三者的MPG宏观图:
在这里,当一个P关联多个G时,就会处理G的执行顺序,就是并发,当一个P在执行一个协程工作时,其他的会在等待,当正在执行的协程遇到阻塞情况,例如IO操作等,go的处理器就会去执行其他的协程,因为对于类似IO的操作,处理器不知道你需要多久才能执行结束,所以他不回去等你执行完。
上面我们看着go的并发好像是抢占式的,事实上go的协程是非抢占式的,由协程主动交出控制权,也就是说,上面在发生IO操作时,并不是调度器强制切换执行其他的协程,而是当前协程交出了控制权,调度器才去执行其他协程。我们列举一下goroutine可能切换的点:
- I/O,select
- channel
- 等待锁
- runtime.Gosched()
-
这些点是go协程可能切换的地方,但是并不是一定切换的。
正是因为是非抢占式的,所以才轻松的构造上万的协程,如果是抢占式,那么就会在切换任务时,保存当前的上下文环境,因为当前线程如果正在做一件事,做到一半,我们就强制停止,这时我们就必须多保存很多信息,避免再次切换回来时任务出错。
线程是操作系统层面的多任务,而go的协程属于编译器层面的多任务,go有自己的调度器来调度。一个协程在哪个线程上是不确定的,这个是由调度器来决定的,多个协程可能在一个或多个线程上运行。
有疑问加站长微信联系(非本文作者)