- 协程的由来
- 协程调度
- golang协程调度器——GPM
- GPM调度原理
协程的由来
在最开始的时候,程序都是串行执行的,即一个程序执行完才能去执行下一个程序,这个时候也是没有多进程\线程的说法的。这种执行模式最大的缺点就是由于执行流程单一,当正在执行的程序阻塞时,cpu只能白白耗着,非常的浪费资源。为了解决这种问题,出现了多进程\线程。
多进程\线程的出现提高了cpu的利用率,当正在执行的程序阻塞时,就可以切换另一个程序进行执行。但是进程的切换代价是比较高的,需要保存一个进程的程序执行上下文、程序计数器、打开的文件描述符……等一系列资源,然后切换到另一个进程。切换成本也是cpu资源的浪费。虽然说线程的切换相比进程而言要轻量化,但是线程的切换仍然在内核态,内核态和用户态之间的切换也要靠cpu。
线程的切换可不可以不用去内核态切换呢?如果线程在用户态就好了。那么就有了用户态的线程,也就是协程,并且比线程要更加的轻量化,且创建一个协程只要几K(创建一个线程要几M)
协程调度
进程和线程实际上是由KSE(kernal schedule entry内核调度实体)来调度的。而协程的切换是发生在用户态的,但是每一个协程还是会对应一个内核态线程最终对应一个KSE(kernal schedule entry内核调度实体)。所以协程和内核态线程就有三种对应关系:一对一、多对一、多对多。
- 一对一:指一个协程对应一个内核态线程。在多处理器的系统上可能会表现很好。但是很多操作系统限制了内核线程的数量,会导致协程数量受到限制。切换时还是要去切换对应的内核态线程。
- 多对一:指多个协程对应一个内核态线程。相比于一对一模型,多对一模型的协程切换速度很快,因为不用去切换底层的内核态线程。但是如果一个协程阻塞,那么其他协程也将阻塞(线程是抢占式的,一个线程阻塞不会影响其他线程,而协程是由用户态调度协作式的,一个协程让出cpu才能执行下一个携程)
- 多对多:多个协程对应多个内核态线程。解决了上述两种模型的缺点。
golang协程调度器——GPM
golang使用的协程是如上第三种 多对多(多个协程对应多个内核态线程)的方式。那golang的协程调度器是怎样让不通的协程进行调度的呢?
golang的协程调度器使用的是GPM模型。
M是 Machine,一个M直接关联一个内核线程,由操作系统管理。
P指的是Processor,代表了M所需的上下文环境,也是处理用户级代码逻辑的处理器。负责衔接M和G的调度上下文,将等待执行的G和M对接。
G指的是Groutine,其实本质也是一种轻量级线程。包括了调用栈、重要的调度信息、例如channel等。
P的数量问题:环境变量$GOMAXPROCS,或在程序中通过 runtime.GOMAXPROCS(runtime.NumCPU())设置
M的数量问题:go语言本身限定M的上限是10000,或在runtime/debug包中的SetMaxThreads()设置。M动态创建,有一个M阻塞(goroutine),会创建一个新的M。
golang控制goroutine的调度并在合适的时间进行GC
GPM调度原理
首先有两个队列,一个global队列,一个P的loacl队列。
当新的goroutine到来时,会直接放到P的local队列中,当local队列满了才会放到global队列中。
然后P从local中取到goroutine去交给M进行处理。