协程是通过使用关键字 go 调用(或执行)一个函数或者方法来实现的(也可以是匿名函数)。
Go 语言在语言层面上支持了并发,goroutine是Go语言提供的一种用户态线程,有时我们也称之为协程。
所谓的协程,某种程度上也可以叫做轻量线程,它不由os而由应用程序创建和管理,因此使用开销较低(一般为4K)。
我们可以创建很多的goroutine,并且它们跑在同一个内核线程之上的时候,就需要一个调度器来维护这些goroutine,确保所有的goroutine都能使用cpu,并且是尽可能公平地使用cpu资源。
调度器的主要有4个重要部分,分别是M、G、P、Sched,前三个定义在runtime.h中,Sched定义在proc.c中。
-
M (work thread) 代表了系统线程OS Thread,由操作系统管理。
-
P (processor) 衔接M和G的调度上下文,它负责将等待执行的G与M对接。P的数量可以通过GOMAXPROCS()来设置,它其实也就代表了真正的并发度,即有多少个goroutine可以同时运行。
-
G (goroutine) goroutine的实体,包括了调用栈,重要的调度信息,例如channel等。
在操作系统的OS Thread和编程语言的User Thread之间,实际上存在3种线程对应模型,也就是:1:1,1:N,M:N。
- N:1 多个(N)用户线程始终在一个内核线程上跑,context上下文切换很快,但是无法真正的利用多核。
- 1:1 一个用户线程就只在一个内核线程上跑,这时可以利用多核,但是上下文切换很慢,切换效率很低。
- M:N 多个goroutine在多个内核线程上跑,这个可以集齐上面两者的优势,但是无疑增加了调度的难度。
M:N 综合两种方式(N:1,1:1)的优势。多个 goroutines 可以在多个 OS threads 上处理。既能快速切换上下文,也能利用多核的优势,而Go正是选择这种实现方式。
MPG是其调度模型
M可以理解为主线程,它是一个物理级别的线程。它比较耗费资源。
p可以理解为在整个执行过程当中的上下文环境。上下文环境可以简单理解为运行时候所需要的资源或者当时操作系统的一个状态。
在主线程运行的过程当中,启动了一个协程,在协程起来的时候需要有一个上下文的环境。上下文环境,就是是否cpu可分配。需要的资源和当时运行的状态。
G是协程
多个m作用在一个cpu,那么就是并发。作用在多个cpu就是并行。可以看到M可以开启多个协程,形成一个队列。
Go 语言中的goroutine是运行在多核CPU中的(通过runtime.GOMAXPROCS(1)设定CPU核数)。 实际中运行的CPU核数未必会和实际物理CPU数相吻合。
每个goroutine都会被一个特定的P(某个CPU)选定维护,而M(物理计算资源)每次挑选一个有效P,然后执行P中的goroutine。
每个P会将自己所维护的goroutine放到一个G队列中,其中就包括了goroutine堆栈信息,是否可执行信息等等。
这里创建的M1线程可能就在其他cpu上了,有点像并行。
协程可以运行在操作系统多个线程之间,也可以运行在线程之内,让你可以很小的内存占用就可以处理大量的任务。由于操作系统线程上的协程时间片,你可以使用少量的操作系统线程就能拥有任意多个提供服务的协程,而且 Go 运行时可以聪明的意识到哪些协程被阻塞了,暂时搁置它们并处理其他协程。
总结:
M代表主线程向下执行,p上下文可以根据系统情况开启协程去工作。M可能有很多,可能全部在一个CPU上面,也可能每个M都在各个不同的CPU上面,这样就叫做并行。
当有协程被阻塞的时候,它有来回切换的一种机制。可以保证主线程的执行,也能够让排队的G协程得到执行的机会。