189 8069 5689

go语言协程池作用 go实现协程池

如何使用Golang进行无心智负担的编程

不知读者是否也会时刻想: 我该怎么写这段代码才优雅, 后期改起来方便?

成都创新互联公司是一家专注于做网站、成都网站建设与策划设计,大峪网站建设哪家好?成都创新互联公司做网站,专注于网站建设十多年,网设计领域的专业建站公司;建站业务涵盖:大峪等地区。大峪做网站价格咨询:028-86922220

努力思考却还是得不到最佳答案, 烦躁等负面情绪不约而来。这便是在编程过程中的心智负担。

这篇文章将从多个方面来简化思考, 希望它能给努力思考的你带来一点小灵感.

java的23种设计模式? 再见.

不是说他们没有作用,只是说它们太死板复杂,学习它们通常入不敷出。

对于编程还有很多需要注意的地方(下文),而不要只局限于设计模式。

我给出的建议是只需要理解一个大概,在平时编程中能用则用。

Golang相比Java来说, 对"面向对象"这件事的支持是"不完整"的.

但话又说回来现在的"面向对象编程"渐渐被扭曲为了"面向类编程"(COP),而COP是复杂并难以理解的,COP有好处但要发挥出来并不容易。所以Golang决定抛弃所有不必要的概念以改善这个问题。

现在不必再理解 封装(这个简单到不需要理解), 多态, 继承.

在golang中只需要理解两个更实在的东西: 接口, 组合.

接口

在Golang中只需要记得一个东西: Interface(接口).

参见io.Reader接口就知道这种设计有多厉害.

读文件是它, 读网络请求也是它, 更骚的是 对于linux(Every thing is a file)来说用它就能操作近乎整个系统了.

简单的说: 当某个功能(如去北京)有多种(或者以后可能有多种)实现方式(如坐火车/飞机/骑车)的时候, 用接口.

组合

组合理解起来并不复杂, 不过是一个语法糖, 就算没有组合功能也毫不影响Go程序的运行.

如下代码, 没有组合换一种写法即可.

简单的说: 组合能用则用,如果你不知道如何使用或者不用也并无大碍。

"开闭原则"对我启发很大.

原文是这样:

但其实我们在开发的时候并不是一直都在和对象打交道.

在我看来, "开闭原则"适用于平时写的任何代码.

完整理解"开闭原则"可能还是会造成心智负担, 所以先打住, 只需要这样:

这便是 "对修改闭合, 对扩展开放".

这里不得不在提及"面向函数编程", 它的思想包括但不限于:

它正好利于修改, 利于写出符合"开闭原则"的代码.

默认的errors包在对于多层的复杂应用是不够的,这种情况下建议自行封装,但别太追求完美 在项目中够用就好。我们等待官方方案即可:

restful能解决大部分命名问题.

你的代码完全可以这样无脑命名而不失优雅.

这样的白话文真的很好命名与理解(根本不需要词汇量).

无脑Goroution, 80%的情况下都没问题.

如果你实在担心, 用channel的做下并发数量控制就好, 或者使用更完整的工具叫"协程池", 他们的实现都不复杂.

得益于golang的开源和这几年的蓬勃发展,golang的生态已经十分完善,所以很多情况下我们应该"面相github编程",第三方提供的代码已能满足我们大多数需求。同时 选用一个受欢迎的第三方代码库通常比自己的更可靠,后续维护也省心很多。

最省心的行为是: 先跟随团队再提出意见

【golang详解】go语言GMP(GPM)原理和调度

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 模型的设计者的文档或直接看源码

参考: ()

()

Go语言设计与实现(上)

基本设计思路:

类型转换、类型断言、动态派发。iface,eface。

反射对象具有的方法:

编译优化:

内部实现:

实现 Context 接口有以下几个类型(空实现就忽略了):

互斥锁的控制逻辑:

设计思路:

(以上为写被读阻塞,下面是读被写阻塞)

总结,读写锁的设计还是非常巧妙的:

设计思路:

WaitGroup 有三个暴露的函数:

部件:

设计思路:

结构:

Once 只暴露了一个方法:

实现:

三个关键点:

细节:

让多协程任务的开始执行时间可控(按顺序或归一)。(Context 是控制结束时间)

设计思路: 通过一个锁和内置的 notifyList 队列实现,Wait() 会生成票据,并将等待协程信息加入链表中,等待控制协程中发送信号通知一个(Signal())或所有(Boardcast())等待者(内部实现是通过票据通知的)来控制协程解除阻塞。

暴露四个函数:

实现细节:

部件:

包: golang.org/x/sync/errgroup

作用:开启 func() error 函数签名的协程,在同 Group 下协程并发执行过程并收集首次 err 错误。通过 Context 的传入,还可以控制在首次 err 出现时就终止组内各协程。

设计思路:

结构:

暴露的方法:

实现细节:

注意问题:

包: "golang.org/x/sync/semaphore"

作用:排队借资源(如钱,有借有还)的一种场景。此包相当于对底层信号量的一种暴露。

设计思路:有一定数量的资源 Weight,每一个 waiter 携带一个 channel 和要借的数量 n。通过队列排队执行借贷。

结构:

暴露方法:

细节:

部件:

细节:

包: "golang.org/x/sync/singleflight"

作用:防击穿。瞬时的相同请求只调用一次,response 被所有相同请求共享。

设计思路:按请求的 key 分组(一个 *call 是一个组,用 map 映射存储组),每个组只进行一次访问,组内每个协程会获得对应结果的一个拷贝。

结构:

逻辑:

细节:

部件:

如有错误,请批评指正。

go 协程池库 ants

在工作中需要并发读取N个小文件,N的数值可大可小。

上线之后,由于在某些场景下,N的数字巨大,开启巨量多的协程会导致系统资源耗尽。所以决定使用协程池的方案,采用了 panjf2000/ants 这个协程库。


新闻名称:go语言协程池作用 go实现协程池
网站链接:http://jkwzsj.com/article/doigjpi.html

其他资讯