golang的线程模型——GMP模型

Python010

golang的线程模型——GMP模型,第1张

内核线程(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线程模型

Goroutine调度是一个很复杂的机制,下面尝试用简单的语言描述一下Goroutine调度机制,想要对其有更深入的了解可以去研读一下源码。

首先介绍一下GMP什么意思:

G ----------- goroutine: 即Go协程,每个go关键字都会创建一个协程。

M ---------- thread内核级线程,所有的G都要放在M上才能运行。

P ----------- processor处理器,调度G到M上,其维护了一个队列,存储了所有需要它来调度的G。

Goroutine 调度器P和 OS 调度器是通过 M 结合起来的,每个 M 都代表了 1 个内核线程,OS 调度器负责把内核线程分配到 CPU 的核上执行

模型图:

避免频繁的创建、销毁线程,而是对线程的复用。

1)work stealing机制

  当本线程无可运行的G时,尝试从其他线程绑定的P偷取G,而不是销毁线程。

2)hand off机制

  当本线程M0因为G0进行系统调用阻塞时,线程释放绑定的P,把P转移给其他空闲的线程执行。进而某个空闲的M1获取P,继续执行P队列中剩下的G。而M0由于陷入系统调用而进被阻塞,M1接替M0的工作,只要P不空闲,就可以保证充分利用CPU。M1的来源有可能是M的缓存池,也可能是新建的。当G0系统调用结束后,根据M0是否能获取到P,将会将G0做不同的处理:

如果有空闲的P,则获取一个P,继续执行G0。

如果没有空闲的P,则将G0放入全局队列,等待被其他的P调度。然后M0将进入缓存池睡眠。

如下图

GOMAXPROCS设置P的数量,最多有GOMAXPROCS个线程分布在多个CPU上同时运行

在Go中一个goroutine最多占用CPU 10ms,防止其他goroutine被饿死。

具体可以去看另一篇文章

【Golang详解】go语言调度机制 抢占式调度

当创建一个新的G之后优先加入本地队列,如果本地队列满了,会将本地队列的G移动到全局队列里面,当M执行work stealing从其他P偷不到G时,它可以从全局G队列获取G。

协程经历过程

我们创建一个协程 go func()经历过程如下图:

说明:

这里有两个存储G的队列,一个是局部调度器P的本地队列、一个是全局G队列。新创建的G会先保存在P的本地队列中,如果P的本地队列已经满了就会保存在全局的队列中;处理器本地队列是一个使用数组构成的环形链表,它最多可以存储 256 个待执行任务。

G只能运行在M中,一个M必须持有一个P,M与P是1:1的关系。M会从P的本地队列弹出一个可执行状态的G来执行,如果P的本地队列为空,就会想其他的MP组合偷取一个可执行的G来执行;

一个M调度G执行的过程是一个循环机制;会一直从本地队列或全局队列中获取G

上面说到P的个数默认等于CPU核数,每个M必须持有一个P才可以执行G,一般情况下M的个数会略大于P的个数,这多出来的M将会在G产生系统调用时发挥作用。类似线程池,Go也提供一个M的池子,需要时从池子中获取,用完放回池子,不够用时就再创建一个。

work-stealing调度算法:当M执行完了当前P的本地队列队列里的所有G后,P也不会就这么在那躺尸啥都不干,它会先尝试从全局队列队列寻找G来执行,如果全局队列为空,它会随机挑选另外一个P,从它的队列里中拿走一半的G到自己的队列中执行。

如果一切正常,调度器会以上述的那种方式顺畅地运行,但这个世界没这么美好,总有意外发生,以下分析goroutine在两种例外情况下的行为。

Go runtime会在下面的goroutine被阻塞的情况下运行另外一个goroutine:

用户态阻塞/唤醒

当goroutine因为channel操作或者network I/O而阻塞时(实际上golang已经用netpoller实现了goroutine网络I/O阻塞不会导致M被阻塞,仅阻塞G,这里仅仅是举个栗子),对应的G会被放置到某个wait队列(如channel的waitq),该G的状态由_Gruning变为_Gwaitting,而M会跳过该G尝试获取并执行下一个G,如果此时没有可运行的G供M运行,那么M将解绑P,并进入sleep状态;当阻塞的G被另一端的G2唤醒时(比如channel的可读/写通知),G被标记为,尝试加入G2所在P的runnext(runnext是线程下一个需要执行的 Goroutine。), 然后再是P的本地队列和全局队列。

系统调用阻塞

当M执行某一个G时候如果发生了阻塞操作,M会阻塞,如果当前有一些G在执行,调度器会把这个线程M从P中摘除,然后再创建一个新的操作系统的线程(如果有空闲的线程可用就复用空闲线程)来服务于这个P。当M系统调用结束时候,这个G会尝试获取一个空闲的P执行,并放入到这个P的本地队列。如果获取不到P,那么这个线程M变成休眠状态, 加入到空闲线程中,然后这个G会被放入全局队列中。

队列轮转

可见每个P维护着一个包含G的队列,不考虑G进入系统调用或IO操作的情况下,P周期性的将G调度到M中执行,执行一小段时间,将上下文保存下来,然后将G放到队列尾部,然后从队列中重新取出一个G进行调度。

除了每个P维护的G队列以外,还有一个全局的队列,每个P会周期性地查看全局队列中是否有G待运行并将其调度到M中执行,全局队列中G的来源,主要有从系统调用中恢复的G。之所以P会周期性地查看全局队列,也是为了防止全局队列中的G被饿死。

除了每个P维护的G队列以外,还有一个全局的队列,每个P会周期性地查看全局队列中是否有G待运行并将其调度到M中执行,全局队列中G的来源,主要有从系统调用中恢复的G。之所以P会周期性地查看全局队列,也是为了防止全局队列中的G被饿死。

M0

M0是启动程序后的编号为0的主线程,这个M对应的实例会在全局变量rutime.m0中,不需要在heap上分配,M0负责执行初始化操作和启动第一个G,在之后M0就和其他的M一样了

G0

G0是每次启动一个M都会第一个创建的goroutine,G0仅用于负责调度G,G0不指向任何可执行的函数,每个M都会有一个自己的G0,在调度或系统调用时会使用G0的栈空间,全局变量的G0是M0的G0

一个G由于调度被中断,此后如何恢复?

中断的时候将寄存器里的栈信息,保存到自己的G对象里面。当再次轮到自己执行时,将自己保存的栈信息复制到寄存器里面,这样就接着上次之后运行了。

我这里只是根据自己的理解进行了简单的介绍,想要详细了解有关GMP的底层原理可以去看Go调度器 G-P-M 模型的设计者的文档或直接看源码

参考: (https://www.cnblogs.com/X-knight/p/11365929.html)

(https://draveness.me/golang/docs/part3-runtime/ch06-concurrency/golang-goroutine/)

原文:【 http://alblue.cn/articles/2020/07/04/1593837537036.html 】

如果有解答的不对的,麻烦各位在评论写出来~

go的调度原理是基于GMP模型,G代表一个goroutine,不限制数量;M=machine,代表一个线程,最大1万,所有G任务还是在M上执行;P=processor代表一个处理器,每一个允许的M都会绑定一个G,默认与逻辑CPU数量相等(通过runtime.GOMAXPROCS(runtime.NumCPU())设置)。

go调用过程:

可以能,也可以不能。

因为go存在不能使用==判断类型:map、slice,如果struct包含这些类型的字段,则不能比较。

这两种类型也不能作为map的key。

类似栈操作,后进先出。

因为go的return是一个非原子性操作,比如语句 return i ,实际上分两步进行,即将i值存入栈中作为返回值,然后执行跳转,而defer的执行时机正是跳转前,所以说defer执行时还是有机会操作返回值的。

select的case的表达式必须是一个channel类型,所有case都会被求值,求值顺序自上而下,从左至右。如果多个case可以完成,则会随机执行一个case,如果有default分支,则执行default分支语句。如果连default都没有,则select语句会一直阻塞,直到至少有一个IO操作可以进行。

break关键字可跳出select的执行。

goroutine管理、信息传递。context的意思是上下文,在线程、协程中都有这个概念,它指的是程序单元的一个运行状态、现场、快照,包含。context在多个goroutine中是并发安全的。

应用场景:

例子参考: https://www.jianshu.com/p/6def5063c1eb

waitgroup

channel

len:切片的长度,访问时间复杂度为O(1),go的slice底层是对数组的引用。

cap:切片的容量,扩容是以这个值为标准。默认扩容是2倍,当达到1024的长度后,按1.25倍。

扩容:每次扩容slice底层都将先分配新的容量的内存空间,再将老的数组拷贝到新的内存空间,因为这个操作不是并发安全的。所以并发进行append操作,读到内存中的老数组可能为同一个,最终导致append的数据丢失。

共享:slice的底层是对数组的引用,因此如果两个切片引用了同一个数组片段,就会形成共享底层数组。当sliec发生内存的重新分配(如扩容)时,会对共享进行隔断。详细见下面例子:

make([]Type,len,cap)

map的底层是hash table(hmap类型),对key值进行了hash,并将结果的低八位用于确定key/value存在于哪个bucket(bmap类型)。再将高八位与bucket的tophash进行依次比较,确定是否存在。出现hash冲撞时,会通过bucket的overflow指向另一个bucket,形成一个单向链表。每个bucket存储8个键值对。

如果要实现map的顺序读取,需要使用一个slice来存储map的key并按照顺序进行排序。

利用map,如果要求并发安全,就用sync.map

要注意下set中的delete函数需要使用 delete(map) 来实现,但是这个并不会释放内存,除非value也是一个子map。当进行多次delete后,可以使用make来重建map。

使用sync.Map来管理topic,用channel来做队列。

参考:

https://studygolang.com/articles/8231

多路归并法:

<pre class="vditor-reset" placeholder="" contenteditable="true" spellcheck="false"><p data-block="0">(1)假设有K路<a href="https://baike.baidu.com/item/%E6%95%B0%E6%8D%AE%E6%B5%81/3002243">数据流</a>,流内部是有序的,且流间同为升序或降序;

</p><p data-block="0">(2)首先读取每个流的第一个数,如果已经EOF,pass;

</p><p data-block="0">(3)将有效的k(k可能小于K)个数比较,选出最小的那路mink,输出,读取mink的下一个;

</p><p data-block="0">(4)直到所有K路都EOF。

</p></pre>

假设文件又1个G,内存只有256M,无法将1个G的文件全部读到内存进行排序。

第一步:

可以分为10段读取,每段读取100M的数据并排序好写入硬盘。

假设写入后的文件为A,B,C...10

第二步:

将A,B,C...10的第一个字符拿出来,对这10个字符进行排序,并将结果写入硬盘,同时记录被写入的字符的文件指针P。

第三步:

将刚刚排序好的9个字符再加上从指针P读取到的P+1位数据进行排序,并写入硬盘。

重复二、三步骤。

go文件读写参考:

https://www.jianshu.com/p/93772ececf65

保证排序前两个相等的数其在序列的前后位置顺序和排序后它们两个的前后位置顺序相同的排序叫稳定排序。

快速排序、希尔排序、堆排序、直接选择排序不是稳定的排序算法。

基数排序、冒泡排序、直接插入排序、折半插入排序、归并排序是稳定的排序算法。

参考:

https://www.jianshu.com/p/abe27f16b7b5

head只请求页面的首部。多用来判断网页是否被修改和超链接的有效性。

get请求页面信息,并返回实例的主体。

参考:

https://blog.csdn.net/ysh1042436059/article/details/80985574

https://www.jianshu.com/p/ef692148a59c

401:未授权的访问。

403: 拒绝访问。

普通的http连接是客户端连接上服务端,然后结束请求后,由客户端或者服务端进行http连接的关闭。下次再发送请求的时候,客户端再发起一个连接,传送数据,关闭连接。这么个流程反复。但是一旦客户端发送connection:keep-alive头给服务端,且服务端也接受这个keep-alive的话,两边对上暗号,这个连接就可以复用了,一个http处理完之后,另外一个http数据直接从这个连接走了。减少新建和断开TCP连接的消耗。这个可以在Nginx设置,

这个keepalive_timout时间值意味着:一个http产生的tcp连接在传送完最后一个响应后,还需要hold住keepalive_timeout秒后,才开始关闭这个连接。

特别注意TCP层的keep alive和http不是一个意思。TCP的是指:tcp连接建立后,如果客户端很长一段时间不发送消息,当连接很久没有收到报文,tcp会主动发送一个为空的报文(侦测包)给对方,如果对方收到了并且回复了,证明对方还在。如果对方没有报文返回,重试多次之后则确认连接丢失,断开连接。

tcp的keep alive可通过

net.ipv4.tcp_keepalive_intvl = 75 // 当探测没有确认时,重新发送探测的频度。缺省是75秒。

net.ipv4.tcp_keepalive_probes = 9 //在认定连接失效之前,发送多少个TCP的keepalive探测包。缺省值是9。这个值乘以tcp_keepalive_intvl之后决定了,一个连接发送了keepalive之后可以有多少时间没有回应

net.ipv4.tcp_keepalive_time = 7200 //当keepalive起用的时候,TCP发送keepalive消息的频度。缺省是2小时。一般设置为30分钟1800

修改:

可以

tcp是面向连接的,upd是无连接状态的。

udp相比tcp没有建立连接的过程,所以更快,同时也更安全,不容易被攻击。upd没有阻塞控制,因此出现网络阻塞不会使源主机的发送效率降低。upd支持一对多,多对多等,tcp是点对点传输。tcp首部开销20字节,udp8字节。

udp使用场景:视频通话、im聊天等。

time-wait表示客户端等待服务端返回关闭信息的状态,closed_wait表示服务端得知客户端想要关闭连接,进入半关闭状态并返回一段TCP报文。

time-wait作用:

解决办法:

close_wait:

被动关闭,通常是由于客户端忘记关闭tcp连接导致。

https://blog.csdn.net/yucdsn/article/details/81092679

根据业务来啊~

重要指标是cardinality(不重复数量),这个数量/总行数如果过小(趋近于0)代表索引基本没意义,比如sex性别这种。

另外查询不要使用select *,根据select的条件+where条件做组合索引,尽量实现覆盖索引,避免回表。

僵尸进程:

即子进程先于父进程退出后,子进程的PCB需要其父进程释放,但是父进程并没有释放子进程的PCB,这样的子进程就称为僵尸进程,僵尸进程实际上是一个已经死掉的进程。

孤儿进程:

一个父进程退出,而它的一个或多个子进程还在运行,那么那些子进程将成为孤儿进程。孤儿进程将被init进程(进程号为1)所收养,并由init进程对它们完成状态收集工作。

子进程死亡需要父进程来处理,那么意味着正常的进程应该是子进程先于父进程死亡。当父进程先于子进程死亡时,子进程死亡时没父进程处理,这个死亡的子进程就是孤儿进程。

但孤儿进程与僵尸进程不同的是,由于父进程已经死亡,系统会帮助父进程回收处理孤儿进程。所以孤儿进程实际上是不占用资源的,因为它终究是被系统回收了。不会像僵尸进程那样占用ID,损害运行系统。

原文链接: https://blog.csdn.net/Eunice_fan1207/article/details/81387417

产生死锁的四个必要条件:

(1) 互斥条件:一个资源每次只能被一个进程使用。

(2) 请求与保持条件:一个进程因请求资源而阻塞时,对已获得的资源保持不放。

(3) 不剥夺条件:进程已获得的资源,在末使用完之前,不能强行剥夺。

(4) 循环等待条件:若干进程之间形成一种头尾相接的循环等待资源关系。

避免方法:

https://blog.csdn.net/IT_10/article/details/97301736

端口占用:lsof -i:端口号 或者 nestat

cpu、内存占用:top

发送信号:kill -l 列出所有信号,然后用 kill [信号变化] [进程号]来执行。如kill -9 453。强制杀死453进程

git log:查看提交记录

git diff :查看变更记录

git merge:目标分支改变,而源分支保持原样。优点:保留提交历史,保留分支结构。但会有大量的merge记录

git rebase:将修改拼接到最新,复杂的记录变得优雅,单个操作变得(revert)很简单;缺点:

git revert:反做指定版本,会新生成一个版本

git reset:重置到某个版本,中间版本全部丢失

etcd、Consul

https://www.cnblogs.com/xybaby/p/10124083.html

pprof

https://www.cnblogs.com/weiweng/p/12497274.html

http://alblue.cn/articles/2020/07/07/1594131614114.html

节省空间(非叶子节点不存储数据,相对b tree的优势),减少I/O次数(节省的空间全部存指针地址,让树变的矮胖),范围查找方便(相对hash的优势)。

explain

其他的见:

https://blog.csdn.net/chengxuyuanyonghu/article/details/61431386

runtime2.go 中关于 p 的定义: 其中 runnext 指针决定了下一个要运行的 g,根据英文的注释大致意思是说:

所以当设置 runtime.GOMAXPROCS(1) 时,此时只有一个 P,创建的 g 依次加入 P, 当最后一个即 i==9 时,加入的最后 一个 g 将会继承当前主 goroutinue 的剩余时间片继续执行,所以会先输出 9, 之后再依次执行 P 队列中其它的 g。

https://studygolang.com/articles/23333?fr=sidebar

方法一:

方法二:

[图片上传失败...(image-4ef445-1594976286098)]

方法1:to_days,返回给的日期从0开始算的天数。

方法2:data_add。向日期添加指定时间间隔

[图片上传失败...(image-b67b10-1594976286098)]

https://blog.csdn.net/itcastcpp/article/details/80462619