重庆分公司,新征程启航
为企业提供网站建设、域名注册、服务器等服务
调度器 由三方面实体构成:
专注于为中小企业提供成都网站设计、成都做网站服务,电脑端+手机端+微信端的三站合一,更高效的管理,为中小企业洛南免费做网站提供优质的服务。我们立足成都,凝聚了一批互联网行业人才,有力地推动了成百上千企业的稳健成长,帮助中小企业通过网站建设实现规模扩充和转变。
三者对应关系:
上图有2个 物理线程 M,每一个 M 都拥有一个上下文(P),每一个也都有一个正在运行的goroutine(G)。
P 的数量可由 runtime.GOMAXPROCS() 进行设置,它代表了真正的并发能力,即可有多少个 goroutine 同时运行。
调度器为什么要维护多个上下文P 呢? 因为当一个物理线程 M 被阻塞时,P 可以转而投奔另一个OS线程 M (即 P 带着 G 连茎拔起,去另一个 M 节点下运行)。这是 Golang调度器厉害的地方,也是高并发能力的保障。
内核线程(Kernel-Level Thread ,KLT)
轻量级进程(Light Weight Process,LWP):轻量级进程就是我们通常意义上所讲的线程,由于每个轻量级进程都由一个内核线程支持,因此只有先支持内核线程,才能有轻量级进程
用户线程与系统线程一一对应,用户线程执行如lo操作的系统调用时,来回切换操作开销相对比较大
多个用户线程对应一个内核线程,当内核线程对应的一个用户线程被阻塞挂起时候,其他用户线程也阻塞不能执行了。
多对多模型是可以充分利用多核CPU提升运行效能的
go线程模型包含三个概念:内核线程(M),goroutine(G),G的上下文环境(P);
GMP模型是goalng特有的。
P与M一般是一一对应的。P(上下文)管理着一组G(goroutine)挂载在M(内核线程)上运行,图中左边蓝色为正在执行状态的goroutine,右边为待执行状态的goroutiine队列。P的数量由环境变量GOMAXPROCS的值或程序运行runtime.GOMAXPROCS()进行设置。
当一个os线程在执行M1一个G1发生阻塞时,调度器让M1抛弃P,等待G1返回,然后另起一个M2接收P来执行剩下的goroutine队列(G2、G3...),这是golang调度器厉害的地方,可以保证有足够的线程来运行剩下所有的goroutine。
当G1结束后,M1会重新拿回P来完成,如果拿不到就丢到全局runqueue中,然后自己放到线程池或转入休眠状态。空闲的上下文P会周期性的检查全局runqueue上的goroutine,并且执行它。
另一种情况就是当有些P1太闲而其他P2很忙碌的时候,会从其他上下文P2拿一些G来执行。
详细可以翻看下方第一个参考链接,写得真好。
最后用大佬的总结来做最后的收尾————
Go语言运行时,通过核心元素G,M,P 和 自己的调度器,实现了自己的并发线程模型。调度器通过对G,M,P的调度实现了两级线程模型中操作系统内核之外的调度任务。整个调度过程中会在多种时机去触发最核心的步骤 “一整轮调度”,而一整轮调度中最关键的部分在“全力查找可运行G”,它保证了M的高效运行(换句话说就是充分使用了计算机的物理资源),一整轮调度中还会涉及到M的启用停止。最后别忘了,还有一个与Go程序生命周期相同的系统监测任务来进行一些辅助性的工作。
浅析Golang的线程模型与调度器
Golang CSP并发模型
Golang线程模型
golang学习笔记
频繁创建线程会造成不必要的开销,所以才有了线程池。在线程池中预先保存一定数量的线程,新任务发布到任务队列,线程池中的线程不断地从任务队列中取出任务并执行,可以有效的减少创建和销毁带来的开销。
过多的线程会导致争抢cpu资源,且上下文的切换的开销变大。而工作在用户态的协程能大大减少上下文切换的开销。协程调度器把可运行的协程逐个调度到线程中执行,同时即时把阻塞的协程调度出协程,从而有效地避免了线程的频繁切换,达到了少量线程实现高并发的效果。
多个协程分享操作系统分给线程的时间片,从而达到充分利用CPU的目的,协程调度器决定了则决定了协程运行的顺序。每个线程同一时刻只能运行一个协程。
go调度模型包含三个实体:
每个处理器维护者一个协程G的队列,处理器依次将协程G调度到M中执行。
每个P会周期性地查看全局队列中是否有G待运行并将其调度到M中执行,全局队列中的G主要来自系统调用中恢复的G.
如果协程发起系统调用,则整个工作线程M被阻塞,协程队列中的其他协程都会阻塞。
一般情况下M的个数会略大于P个数,多出来的M将会在G产生系统调用时发挥作用。与线程池类似,Go也提供M池子。当协程G1发起系统掉用时,M1会释放P,由 M1-P-G1 G2 ... 转变成 M1-G1 , M2会接管P的其他协程 M2-P-G2 G3 G4... 。
冗余的M可能来源于缓存池,也可能是新建的。
当G1结束系统调用后,根据M1是否获取到P,进行不用的处理。
多个处理P维护队列可能不均衡,导致部分处理器非常繁忙,而其余相对空闲。产生原因是有些协程自身不断地派生协程。
为此Go调度器提供了工作量窃取策略,当某个处理器P没有需要调度的协程时,将从其他处理中偷取协程,每次偷取一半。
抢占式调度,是指避免某个协程长时间执行,而阻碍其他协程被调度的机制。
调度器监控每个协程执行时间,一旦执行时间过长且有其他协程等待,会把协程暂停,转而调度等待的协程,以达到类似时间片轮转的效果。比如for循环会一直占用执行权。
在IO密集型应用,GOMAXPROCS大小设置大一些,获取性能会更好。
IO密集型会经常发生系统调用,会有一个新的M启用或创建,但由于Go调度器检测M到被阻塞有一定延迟。如果P数量多,则P管理协程队列会变小。
1. 保留但大幅度简化指针
Go语言保留着C中值和指针的区别,但是对于指针繁琐用法进行了大量的简化,引入引用的概念。所以在Go语言中,你几乎不用担心会因为直接操作内寸而引起各式各样的错误。
2. 多参数返回
还记得在C里面为了回馈多个参数,不得不开辟几段指针传到目标函数中让其操作么?在Go里面这是完全不必要的。而且多参数的支持让Go无需使用繁琐的exceptions体系,一个函数可以返回期待的返回值加上error,调用函数后立刻处理错误信息,清晰明了。
3. Array,slice,map等内置基本数据结构
如果你习惯了Python中简洁的list和dict操作,在Go语言中,你不会感到孤单。一切都是那么熟悉,而且更加高效。如果你是C++程序员,你会发现你又找到了STL的vector 和 map这对朋友。
4. Interface
Go语言最让人赞叹不易的特性,就是interface的设计。任何数据结构,只要实现了interface所定义的函数,自动就implement了这个interface,没有像Java那样冗长的class申明,提供了灵活太多的设计度和OO抽象度,让你的代码也非常干净。千万不要以为你习惯了Java那种一条一条加implements的方式,感觉还行,等接口的设计越来越复杂的时候,无数Bug正在后面等着你。
同时,正因为如此,Go语言的interface可以用来表示任何generic的东西,比如一个空的interface,可以是string可以是int,可以是任何数据类型,因为这些数据类型都不需要实现任何函数,自然就满足空interface的定义了。加上Go语言的type assertion,可以提供一般动态语言才有的duck typing特性, 而仍然能在compile中捕捉明显的错误。
5. OO
Go语言本质上不是面向对象语言,它还是过程化的。但是,在Go语言中, 你可以很轻易的做大部分你在别的OO语言中能做的事,用更简单清晰的逻辑。是的,在这里,不需要class,仍然可以继承,仍然可以多态,但是速度却快得多。因为本质上,OO在Go语言中,就是普通的struct操作。
6. Goroutine
这个几乎算是Go语言的招牌特性之一了,我也不想多提。如果你完全不了解Goroutine,那么你只需要知道,这玩意是超级轻量级的类似线程的东西,但通过它,你不需要复杂的线程操作锁操作,不需要care调度,就能玩转基本的并行程序。在Go语言里,触发一个routine和erlang spawn一样简单。基本上要掌握Go语言,以Goroutine和channel为核心的内存模型是必须要懂的。不过请放心,真的非常简单。
7. 更多现代的特性
和C比较,Go语言完全就是一门现代化语言,原生支持的Unicode, garbage collection, Closures(是的,和functional programming language类似), function是first class object,等等等等。
看到这里,你可能会发现,我用了很多轻易,简单,快速之类的形容词来形容Go语言的特点。我想说的是,一点都不夸张,连Go语言的入门学习到提高,都比别的语言门槛低太多太多。在大部分人都有C的背景的时代,对于Go语言,从入门到能够上手做项目,最多不过半个月。Go语言给人的感觉就是太直接了,什么都直接,读源代码直接,写自己的代码也直接。
参考:
Goroutine并发调度模型深度解析手撸一个协程池
Golang 的 goroutine 是如何实现的?
Golang - 调度剖析【第二部分】
OS线程初始栈为2MB。Go语言中,每个goroutine采用动态扩容方式,初始2KB,按需增长,最大1G。此外GC会收缩栈空间。
BTW,增长扩容都是有代价的,需要copy数据到新的stack,所以初始2KB可能有些性能问题。
更多关于stack的内容,可以参见大佬的文章。 聊一聊goroutine stack
用户线程的调度以及生命周期管理都是用户层面,Go语言自己实现的,不借助OS系统调用,减少系统资源消耗。
Go语言采用两级线程模型,即用户线程与内核线程KSE(kernel scheduling entity)是M:N的。最终goroutine还是会交给OS线程执行,但是需要一个中介,提供上下文。这就是G-M-P模型
Go调度器有两个不同的运行队列:
go1.10\src\runtime\runtime2.go
Go调度器根据事件进行上下文切换。
调度的目的就是防止M堵塞,空闲,系统进程切换。
详见 Golang - 调度剖析【第二部分】
Linux可以通过epoll实现网络调用,统称网络轮询器N(Net Poller)。
文件IO操作
上面都是防止M堵塞,任务窃取是防止M空闲
每个M都有一个特殊的G,g0。用于执行调度,gc,栈管理等任务,所以g0的栈称为调度栈。g0的栈不会自动增长,不会被gc,来自os线程的栈。
go1.10\src\runtime\proc.go
G没办法自己运行,必须通过M运行
M通过通过调度,执行G
从M挂载P的runq中找到G,执行G
线程:
多线程是为了解决CPU利用率的问题,线程则是为了减少上下文切换时的开销,进程和线程在Linux中没有本质区别,最大的不同就是进程有自己独立的内存空间,而线程是共享内存空间。
在进程切换时需要转换内存地址空间,而线程切换没有这个动作,所以线程切换比进程切换代价要小得多。
协程:
想要简单,又要性能高,协程就可以达到我们的目的,它是用户视角的一种抽象,操作系统并没有这个概念,主要思想是在用户态实现调度算法,用少量线程完成大量任务的调度。
Goroutine是GO语言实现的协程,其特点是在语言层面就支持,使用起来十分方便,它的核心是MPG调度模型:M即内核线程;P即处理器,用来执行Goroutine,它维护了本地可运行队列;G即Goroutine,代码和数据结构;S及调度器,维护M和P的信息。