【导读】go并发调度模型面试中高频问题,本文比较详细地分析了go语言gmp和调度器的实现。
并发,编程里面最核心的主题,一直以来都是被开发者谈及最多的话题;go语言是通过goroutine实现并发编程,goroutine具有消耗资源低、运行效率高等特点;官方宣称原生goroutine并发成千上万不成问题。那么,理解goroutine的调度模型和工作原理,对于写go代码显得非常重要。
当一个程序运行时,操作系统会为程序启动一个进程,可以把进程看作是一个包含了应用程序运行时需要用到的各种资源的容器;这些资源包括但不限于内存空间、句柄、线程。一个线程是一个执行空间,这个空间会被操作系统调度来执行代码。对操作系统而言,它的眼里只有线程,操作系统会在物理处理器上调度线程来运行。
goroutine,是Go语言并发编程的实现,是一种用户层轻量级线程或者说是类协程。Go程序对操作系统来说只是一个用户层程序,它甚至不知道goroutine的存在。Go程序本身是运行在一个或多个操作系统线程上,所以Go调度器需要将众多goroutine按照一定的算法调度到操作系统的线程上执行。这种在语言层面自带调度器的,称之为原生支持并发。
G-P-M 调度模型由Go抽象出来的实现,最终形成了Go调度器的基本结构:
当正在运行的goroutine需要执行一个阻塞的系统调用,如打开/读写文件;线程和goroutine会从逻辑处理器P上分离,该线程会继续阻塞,等待系统调用返回;
此时,逻辑处理器失去了用来运行的线程,调度器(runtime)会创建一个新的线程,并将其绑定到这个逻辑处理器上;
之后,逻辑处理器P会从本地队列选取另一个goroutine来运行。一旦阻塞的系统调用执行完成并返回,对应的goroutine会放回本地运行队列,线程会保存好,以便之后继续使用。
以上是从宏观的角度对Goroutine和基本调度过程进行的一些概要性的总结,Go的调度有很多复杂的抢占式调度、阻塞调度的细节,后续再去找相关资料深入理解。
栈是用来存储当前正在运行或挂起的函数的空间,操作系统会为每个线程分配固定大小(一般是2MB)的内存块做栈。2MB的固定空间,对于小小的goroutine是很大的浪费,对于复杂的任务来说又明显不够用。
goroutine的栈不是固定的,一开始以一个很小的栈空间(2KB)开启生命周期,栈的大小会根据需要动态伸缩。和操作系统线程的栈有同样作用,会保存当前正在运行或挂起的函数的本地变量。
线程会被操作系统内核调度到处理器上运行,每几毫秒会发生一次硬件计时器中断,当前线程需要让出CPU并将线程状态保存到寄存器中,CPU继续处理其他线程任务,在此线程下一次获得CPU执行时间,会从寄存器中恢复该线程上次的的状态并继续执行。线程在内核切换上下文是很慢的。
Go调度器是在其本身运行的用户层进行调度的,不需要进入内核的上下文切换,调度成本会低很多。
转自:大黄蜂
zhuanlan.zhihu.com/p/57875135
- EOF -
Go 开发大全
参与维护一个非常全面的Go开源技术资源库。日常分享 Go, 云原生、k8s、Docker和微服务方面的技术文章和行业动态。
关注后获取
回复 Go 获取6万star的Go资源库
分享、点赞和在看
支持我们分享更多好文章,谢谢!