在单进程时代,执行流程单一,计算机只能一个任务一个任务去处理,一切程序只能串行执行,进程阻塞会带来CPU时间浪费;在多进程/线程时代,当一个进程阻塞时,切换到另外等候的进程,时间片轮转法保证了等待的进程都能够被运行,但是进程间的调度会占用CPU大部分时间;在高并发场景下,如果为每个任务都去创建线程是不现实的。是否能在线程基础上再做划分呢?我们将一个线程切分为用户线程(co-routine协程)和内核线程(thread线程),将其绑定在一起,CPU只去操作内核线程thread。
既然能够绑定一个,是否多个协程可以绑定在一个或者多个线程上呢?
N:1关系中thread绑定调度器,由协程调度器连接多个协程,弊端是由于协程调度器轮询访问,当有一个协程阻塞,会导致后续协程访问不到;
M:N关系中多个线程通过协程调度器绑定多个协程,那么这种方案的重点在于对协程调度器的优化。
协程与线程的区别之一是,线程由CPU调度是抢占式的,协程由用户态调度是协作式的,一个协程让出CPU后才执行下一个协程。
Goalng中的goroutine与协程co-routine相比除了名称不一样外,在内存方面做了优化,一个goroutine只占几KB,这表示可以存在大量goroutine,而且调度更为灵活(runtime调度)。
在go中goroutine调度器使用了GMP模型。其中G表示goroutine协程、P表示processor处理器,包含了每个goroutine的资源如栈、堆等,如果线程想运行goroutine,必须先获取P,P中还包含了可运行的G队列;M表示thread内核线程。
这里P的数量最大值为CPU核心数,当然这个值可以通过环境变量$GOMAXPROCS配置,或者在程序中通过runtime.GOMAXPROCS()来设置。
协程调度器复用线程时线程可以调用以下两种机制:
work stealing机制:当本线程无可运行的G时,尝试先从本地的其他线程绑定的P中偷取G,而不是销毁线程;如果从其他P偷不到G时,它可以从全局队列中获取G,对于偷取数量可以偷一半或者一半以上。
hand off机制:当本线程因为G进行系统调用阻塞时,线程释放绑定的P,把P转移给其他空闲的线程执行,这个空闲线程可以从休眠M队列中取,如果该队列为空则需创建一个线程,来接收P以及P绑定的可运行的G队列。
以main goroutine为例来看一个go func()调度流程:
package main
import "fmt"
func main() {
fmt.Println("Hello world")
}
1、runtime创建第一个线程M0:M0是启动进程后的编号为0的主线程,这个M对应的实例会在全局变量runtime.m0中,不需要在heap上分配,M0负责执行初始化操作和启动第一个G,在之后M0就和其他的M一样了。
2、runtime创建第一个Go协程G0:G0是每次启动一个M都会第一个创建的goroutine,G0仅用于负责调度G,G0不指向任何可执行函数,每个M都会有一个自己的G0。在调度或系统调用时会使用G0的栈空间,全局变量的G0是M0的G0。一般的G0放在本地队列中。
3、关联M0和G0。
4、调度初始化初始化M0、栈、垃圾回收,以及创建和初始化由GOMAXPROCS个P构成的P列表。
5、创建main()中的goroutine,即runtime.main创建goroutine。
6、启动M0,此时M0已经绑定了P,从P的本地队列中获取G,获取到main goroutine。
7、M绑定P。
8、循环判断M通过P是否能够获取到G。
9、获取不到则M进入休眠队列,等待被唤醒后再重新与P绑定。
10、能够获取到G,则M根据G中的栈信息和调度信息设置运行环境。
11、M执行G。
12、G退出,runtime.main执行Defer和Panic处理,或调用runtime.exit退出程序。
通过Debug trace查看GMP信息,运行以下代码为例:
package main
import (
"fmt"
"time"
)
func main() {
for i := 0; i < 5; i++ {
time.Sleep(time.Second)
fmt.Println("hello GMP")
}
}
我们要循环输出5次“hello GMP”,在终端go build之后,执行:
GODEBUG=schedtrace=1000 ./trace2
trace2是可执行文件,1000表示1000毫秒,结果如下:
SCHED 0ms: gomaxprocs=8 idleprocs=5 threads=5 spinningthreads=1 idlethreads=0 runqueue=0 [1 0 0 0 0 0 0 0]
hello GMP
SCHED 1001ms: gomaxprocs=8 idleprocs=8 threads=5 spinningthreads=0 idlethreads=3 runqueue=0 [0 0 0 0 0 0 0 0]
hello GMP
SCHED 2005ms: gomaxprocs=8 idleprocs=8 threads=5 spinningthreads=0 idlethreads=3 runqueue=0 [0 0 0 0 0 0 0 0]
hello GMP
SCHED 3012ms: gomaxprocs=8 idleprocs=8 threads=5 spinningthreads=0 idlethreads=3 runqueue=0 [0 0 0 0 0 0 0 0]
hello GMP
SCHED 4013ms: gomaxprocs=8 idleprocs=8 threads=5 spinningthreads=0 idlethreads=3 runqueue=0 [0 0 0 0 0 0 0 0]
hello GMP
https://www.kancloud.cn/aceld/golang 刘丹冰 《GOLANG修养之路》
END