可增长的栈
OS
线程(操作系统线程)一般都有固定的栈内存(通常为2MB
)。一个goroutine
的栈在其生命周期开始时只有很小的栈(典型情况下2KB
),goroutine
的栈不是固定的,它可以按需增大和缩小,goroutine
的栈大小限制可以达到1GB,虽然极少会用到这么大。所以在Go
语言中一次创建十万左右的 goroutine
也是可以的
goroutine调度
GPM
是Go
语言运行时(runtime
)层面的实现,是go
语言自己实现的一套调度系统。区别于操作系统调度OS
线程。
G
很好理解,就是个goroutine
的,里面除了存放本goroutine
信息外 还有与所在P
的绑定等信息。P
管理着一组goroutine
队列,P
里面会存储当前goroutine
运行的上下文环境(函数指针,堆栈地址及地址边界),P
会对自己管理的goroutine
队列做一些调度(比如把占用CPU
时间较长的goroutine
暂停、运行后续的goroutine
等等)当自己的队列消费完了就去全局队列里取,如果全局队列里也消费完了会去其他P
的队列里抢任务。M
(machine
)是Go
运行时(runtime
)对操作系统内核线程的虚拟,M
与内核线程一般是一一映射的关系, 一个groutine
最终是要放到M
上执行的;
P与M一般也是一一对应的。
他们关系是: P管理着一组G挂载在M上运行。当一个G长久阻塞在一个M上时,runtime会新建一个M,阻塞G所在的P会把其他的G 挂载在新建的M上。当旧的G阻塞完成或者认为其已经死掉时 回收旧的M。
P的个数是通过runtime.GOMAXPROCS设定(最大256),Go1.5版本之后默认为物理线程数。 在并发量大的时候会增加一些P和M,但不会太多,切换太频繁的话得不偿失。
单从线程调度讲,Go语言相比起其他语言的优势在于OS线程是由OS内核来调度的,goroutine则是由Go运行时(runtime)自己的调度器调度的,这个调度器使用一个称为m:n调度的技术(复用/调度m个goroutine到n个OS线程)。 其一大特点是goroutine的调度是在用户态下完成的, 不涉及内核态与用户态之间的频繁切换,包括内存的分配与释放,都是在用户态维护着一块大的内存池, 不直接调用系统的malloc函数(除非内存池需要改变),成本比调度OS线程低很多。 另一方面充分利用了多核的硬件资源,近似的把若干goroutine均分在物理线程上, 再加上本身goroutine的超轻量,以上种种保证了go调度方面的性能。
GOMAXPROCS
Go
运行时的调度器使用GOMAXPROCS
参数来确定需要使用多少个OS
线程来同时执行Go
代码。默认值是机器上的CPU
核心数。例如在一个8核心的机器上,调度器会把Go代码同时调度到8个OS线程上(GOMAXPROCS是m:n调度中的n)。
Go语言中可以通过runtime.GOMAXPROCS()函数设置当前程序并发时占用的CPU逻辑核心数。
Go1.5版本之前,默认使用的是单核心执行。Go1.5版本之后,默认使用全部的CPU逻辑核心数。
我们可以通过将任务分配到不同的CPU逻辑核心上实现并行的效果,这里举个例子:
func a() {
for i := 1; i < 10; i++ {
fmt.Println("A:", i)
}
}
func b() {
for i := 1; i < 10; i++ {
fmt.Println("B:", i)
}
}
func main() {
runtime.GOMAXPROCS(1)
go a()
go b()
time.Sleep(time.Second)
}
输出结果如下
两个任务只有一个逻辑核心,此时是做完一个任务再做另一个任务。 将逻辑核心数设为2,此时两个任务并行执行,代码如下。
➜ go run main.go
B: 1
B: 2
B: 3
B: 4
B: 5
B: 6
B: 7
B: 8
B: 9
A: 1
A: 2
A: 3
A: 4
A: 5
A: 6
A: 7
A: 8
A: 9
func a() {
for i := 1; i < 10; i++ {
fmt.Println("A:", i)
}
}
func b() {
for i := 1; i < 10; i++ {
fmt.Println("B:", i)
}
}
func main() {
runtime.GOMAXPROCS(2)
go a()
go b()
time.Sleep(time.Second)
}
输出如下
Go语言中的操作系统线程和goroutine的关系:
$ go run main.go
B: 1
B: 2
B: 3
B: 4
A: 1
A: 2
A: 3
A: 4
A: 5
A: 6
A: 7
A: 8
A: 9
B: 5
B: 6
B: 7
B: 8
B: 9
- 一个操作系统线程对应用户态多个goroutine。
- go程序可以同时使用多个操作系统线程。
- goroutine和OS线程是多对多的关系,即m:n。