Goroutine是Go语言的一大杀器,是Go语言的基石之一,Go语言之所以这么高效与Goroutine的调度器息息相关。
首先,先说一下Goroutine与常说的线程有什么关系和区别。
1.内存开销。创建一个goroutine只需要消耗2KB的栈内存,而创建一个线程往往需要MB级别的栈内存。在某些场景下,有时候并不需要如此多栈内存,如果MB级别的栈内存,会造成大量的浪费。而goroutine在实际的运行过程中,如果说2KB的栈空间不够,其内部会自动扩容。
2.创建和销毁。goroutine由Go runtime管理,属于用户级。而线程的创建和销毁要和操作系统打交道,资源消耗大,属于内核级。
3.切换。线程切换需要多个寄存器来方便恢复,而Goroutine只需要三个寄存器:Program Counter 、 Stack Pointer 、BP。正常情况下,线程切换需要消耗超过1000ns,而goroutine切换只需要约200ns。
GPM调度模型
GPM是Go语言运行时runtime层面的实现,是go语言自己实现的一套调度系统,区别于操作系统调度OS线程。
- G是goroutine,里面除了存放goroutine信息外,还有与所在P的绑定等信息。
- P管理着一组goroutine队列,P里面会存储当前goroutine运行的上下文环境,例如函数指针,堆栈地址及地址边界等。P会对自己管理的goroutine队列做一些调度,比如:把占用CPU时间较长的goroutine暂停、运行后续goroutine等等当自己的队列消费完了就去全局队列里取。
- M是machine,Go运行时runtime对操作系统内核线程的虚拟。 M与内核线程一般是一一映射的关系, groutine最终是在M上执行的。
从线程调度讲,goroutine是由Go运行时runtime自己的调度器调度的,这个调度器使用一个称为m:n调度的技术即复用或者调度m个goroutine到n个OS线程。 特点是goroutine的调度是在用户态下完成的, 不涉及内核态与用户态之间的频繁切换,包括内存的分配与释放,都是在用户态维护着一块大的内存池, 不直接调用系统的malloc函数,除非内存池需要改变,成本比调度OS线程低很多。 另一方面充分利用了多核的硬件资源,近似的把若干goroutine均分在物理线程上, 再加上本身goroutine的超轻量,以上种种保证了go调度方面的性能。