可增长的栈

OS 线程(操作系统线程)一般都有固定的栈内存(通常为2MB)。一个goroutine的栈在其生命周期开始时只有很小的栈(典型情况下2KB),goroutine 的栈不是固定的,它可以按需增大和缩小,goroutine的栈大小限制可以达到1GB,虽然极少会用到这么大。所以在Go语言中一次创建十万左右的 goroutine 也是可以的

goroutine调度

GPMGo语言运行时(runtime)层面的实现,是go语言自己实现的一套调度系统。区别于操作系统调度OS线程。

  • G很好理解,就是个goroutine的,里面除了存放本goroutine信息外 还有与所在P的绑定等信息。
  • P管理着一组goroutine队列,P里面会存储当前goroutine运行的上下文环境(函数指针,堆栈地址及地址边界),P会对自己管理的goroutine队列做一些调度(比如把占用CPU时间较长的goroutine暂停、运行后续的goroutine等等)当自己的队列消费完了就去全局队列里取,如果全局队列里也消费完了会去其他P的队列里抢任务。
  • Mmachine)是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逻辑核心上实现并行的效果,这里举个例子:

  1. func a() {
  2. for i := 1; i < 10; i++ {
  3. fmt.Println("A:", i)
  4. }
  5. }
  6. func b() {
  7. for i := 1; i < 10; i++ {
  8. fmt.Println("B:", i)
  9. }
  10. }
  11. func main() {
  12. runtime.GOMAXPROCS(1)
  13. go a()
  14. go b()
  15. time.Sleep(time.Second)
  16. }

输出结果如下

  1. go run main.go
  2. B: 1
  3. B: 2
  4. B: 3
  5. B: 4
  6. B: 5
  7. B: 6
  8. B: 7
  9. B: 8
  10. B: 9
  11. A: 1
  12. A: 2
  13. A: 3
  14. A: 4
  15. A: 5
  16. A: 6
  17. A: 7
  18. A: 8
  19. A: 9
两个任务只有一个逻辑核心,此时是做完一个任务再做另一个任务。 将逻辑核心数设为2,此时两个任务并行执行,代码如下。
  1. func a() {
  2. for i := 1; i < 10; i++ {
  3. fmt.Println("A:", i)
  4. }
  5. }
  6. func b() {
  7. for i := 1; i < 10; i++ {
  8. fmt.Println("B:", i)
  9. }
  10. }
  11. func main() {
  12. runtime.GOMAXPROCS(2)
  13. go a()
  14. go b()
  15. time.Sleep(time.Second)
  16. }

输出如下

  1. $ go run main.go
  2. B: 1
  3. B: 2
  4. B: 3
  5. B: 4
  6. A: 1
  7. A: 2
  8. A: 3
  9. A: 4
  10. A: 5
  11. A: 6
  12. A: 7
  13. A: 8
  14. A: 9
  15. B: 5
  16. B: 6
  17. B: 7
  18. B: 8
  19. B: 9
Go语言中的操作系统线程和goroutine的关系:
  1. 一个操作系统线程对应用户态多个goroutine。
  2. go程序可以同时使用多个操作系统线程。
  3. goroutine和OS线程是多对多的关系,即m:n。