前言
学到这里,我们需要了解一下OS线程一般都是有属于自己的线程栈内存(通常为2MB)。一个goroutine栈在刚启动的时候确实很小的(通常是2KB),所以一次性创建10w个goroutine也是没有问题的,随着运行goroutine栈最大可达到1GB。goroutine属于用户态线程,go语言在runtime层面通过实现了一个GPM机制来对goroutine进行调度。
goroutine的调度机制
首先来大致解释下什么是GPM
G:就是goroutine,包含goroutine信息以及与所在P的绑定等信息
P:管理着一组goroutine的队列,P里面会存储当前goroutine运行的上下文环境(函数指针,堆栈地址以及地址边界),P内部对自己管理的一些列G进行调度,当自己管理的队列消费完了还会去全局队列中取,如果全局队列也消费完了还会去其他的P中取任务
M:是Golang中对os内核线程的抽象,M与内核线程一般就是11对应的关系,goroutine最终都是会在M上进行执行的
我们来稍微理解一下这个GPM模型,本人会觉得与java中的线程池模型还是很相似的,只是本人研究的java线程池中并无全局队列以及去其他队列抢任务的概念出现,了解jJAVA中线程池工作原理戳这里。在GPM中,P和M通常也是11对应的,但这并不是绝对的,可以这么通俗的来理解一个队列P对应一个os线程M,os线程由操作系统cpu运行,然后P来对队列中的任务goroutine进行调度,让每一个goroutine在os线程上不停的来回切换运行(准确一点的表达:P管理着一组G挂载在M上运行,当一个G长久阻塞在一个M上时,runtime会新建一个M,阻塞G所在的P会把其他的G挂载在新建的M上去。当旧的G阻塞完成或者认为其已经死掉时那么就会回收旧的M)。
GOMAXPROCS
在前言中我们说了G的内存大小以及创建数量等问题,那么P由这方面的限制吗?P的个数是通过runtime.GOMAXPROCS设定(最大256),Go1.5版本之后默认为物理线程数。在并发量大的时候会适当增加一些P和M,但是也不会太多,切换太频繁也没有太多必要。
实例查看效果
Go运行时的调度器使用GOMAXPROCS
参数来确定需要使用多少个OS线程来同时执行Go代码。默认值是机器上的CPU核心数。例如在一个8核cpu上,调度器会把Go代码同时调度到8个OS线程上(GOMAXPROCS是m:n调度中的n)。
Go语言中可以通过runtime.GOMAXPROCS()
函数设置当前程序并发时占用的CPU逻辑核心数。
Go1.5版本之前,默认使用的是单核心执行。Go1.5版本之后,默认使用全部的CPU逻辑核心数。
我们可以通过将任务分配到不同的CPU逻辑核心上实现并行的效果,这里我们来举个例子看看。
例1:
1 | package main |
上面例子中我们设置了runtime.GOMAXPROCS()为1,也就是说GPM模型中最多一个OS线程在运行,可以很明显的看到Agoroutine和Bgoroutine是串行打印,原因也很简单,两个goroutine在一个OS线程上进行调度的。下面我们将runtime.GOMAXPROCS()设置成2,两个goroutine在两个OS线程上进行调度,这里其实可以理解在OS线程层面并行。
例2:
1 | package main |
再次运行上述代码,发现AB变成了交替打印。各种意味自行理解哈~