最近看了看go scheduler的基本原理,本文介绍go语言scheduler的基本原理以及如何查看go代码中的go routine的执行情况。
0)Scheduler(调度器)
熟悉go语言的小伙伴应该都使用过goroutine。goroutine就是Go语言提供的一种用户态线程。Scheduler是调度goroutine的调度器。
Go的调度器内部有三个重要概念:M,P,G。
M (machine): 代表真正的内核操作系统里面的线程,和POSIX里的thread差不多,也是真正执行goroutine逻辑的部分。
G (Goroutine): 代表一个goroutine。
P (Processor): 代表调度的上下文,可以理解成一个局部调度器。
Go语言实现了多个Goroutine到多个Processor的映射(调度)。注意的是,针对X个Processor,Scheduler可能创建多于X个M(有些M可能会暂时被block)。还需要理解额外两个概念:GRQ(Global Running Queue)以及 LRQ(Local Running Queue)。未指定Processor的Goroutine会存放在GRQ上,在调度到合适的Processor后,会将一个Goroutine从GRQ移动到LRQ。
Go程序中发生了四类事件,允许调程序做出调度决策。
a. 使用关键字go b. 垃圾收集 c. 系统调用 d. 同步
1)Processor的个数
Processor的个数可以通过GOMAXPROCS环境变量设置。GOMAXPROCS默认值是CPU的核数。Processor的个数可以通过如下的go代码进行查询:
package main
import(
"fmt"
"runtime"
)
func main(){
// NumCPU returns the number of logical
// CPUs usable by the current process.
fmt.Println(runtime.NumCPU())
}
也就是通过runtime.NumCPU函数可以获得Processor的个数。查看go语言的源代码(runtime/os_linux.c),NumCPU函数的实现函数如下:
funcgetproccount()int32 {
const maxCPUs = 64 * 1024
var buf[maxCPUs / 8]byte
r :=sched_getaffinity(,unsafe.Sizeof(buf),&buf[])
if r
return 1
}
n := int32()
for _,v := range buf[:r]{
for v != 0 {
n += int32(v & 1)
}
}
if n == 0 {
n = 1
}
return n
}
在linux操作系统中,通过调用sched_getaffinity函数获取cpu_set_t的信息,从而推算出CPU的个数。
2)查看Processor以及Goroutine的调度情况
在go程序命令行加上GODEBUG=schedtrace=1000,scheddetail=1选项,可以实时打印Goroutine的调度情况:schedtrace参数指定打印的时间间隔,scheddetail参数指定是否打印更多细节。
以下面的代码为例:
package main
import "fmt"
import "time"
func f(from string,index int){
fmt.Println("index: ",index)
var i int=0
for true {
i = i + 1
//time.Sleep(1 * time.Second)
}
}
func main(){
var i int= 0
for true {
go f("goroutine",i)
i+=1
time.Sleep(1 * time.Second)
}
}
这段go程序,不停的创建Goroutine,每个Goroutine执行f函数。注意,f函数中也是一个for循环(默认的情况下Sleep函数不起作用)。
如果只用schedtrace=1000参数,在MAC笔记本上的输出结果如下:
index: 0
index: 1
SCHED 0ms: gomaxprocs=4 idleprocs=2 threads=5 spinningthreads=0 idlethreads=1 runqueue=0[0 0 0 0]
index: 2
SCHED 1009ms: gomaxprocs=4 idleprocs=1 threads=6 spinningthreads=0 idlethreads=1 runqueue=0[0 0 0 0]
index: 3
SCHED 2019ms: gomaxprocs=4 idleprocs=0 threads=6 spinningthreads=0 idlethreads=0 runqueue=0[0 0 0 0]
SCHED 3020ms: gomaxprocs=4 idleprocs=0 threads=6 spinningthreads=0 idlethreads=1 runqueue=1[0 0 0 0]
SCHED 4021ms: gomaxprocs=4 idleprocs=0 threads=6 spinningthreads=0 idlethreads=1 runqueue=1[0 0 0 0]
SCHED 5030ms: gomaxprocs=4 idleprocs=0 threads=6 spinningthreads=0 idlethreads=1 runqueue=1[0 0 0 0]
一行SCHED开头的log打印出了当前时刻Scheduler的状态:
gomaxprocs:多少个Processor
idleProcs:多少个空闲的Processor
threads:多少个M
idlethreads:多少个空闲thread
runqueue:GRQ以及LRQ的状态,第一个数字是GRQ中的Goroutine的数量,[ ] 中的数字是各个LRQ中的Goroutine的数量。
你会发现上述的go代码中只跑了4个Goroutine,不会创建新的Goroutine,而且GRQ中一直有个Goroutine永远调度不到。原因是f函数是个死循环,不会释放Processor。
如果在f函数中加入延时,则会不停的创建以及调度Goroutine。
如果使用scheddetail=1,会打印出具体的Thread的执行信息。
总结:goroutine就是Go语言提供的一种用户态线程。Scheduler是调度goroutine的调度器。Go的调度器内部由三部分组成:M(Machine),P(Processor),G(Goroutine)。在go程序命令行加上GODEBUG=schedtrace=1000,scheddetail=1选项,可以实时打印Goroutine的调度情况。
领取专属 10元无门槛券
私享最新 技术干货