Golang协程池gopool怎么设计与实现

Golang协程池gopool怎么设计与实现

这篇文章主要介绍“Golang协程池gopool怎么设计与实现”,在日常操作中,相信很多人在Golang协程池gopool怎么设计与实现问题上存在疑惑,小编查阅了各式资料,整理出简单好用的操作方法,希望对大家解答”Golang协程池gopool怎么设计与实现”的疑惑有所帮助!接下来,请跟着小编一起来学习吧!

Goroutine

Goroutine 是 Golang 提供的一种轻量级线程,我们通常称之为「协程」,相比较线程,创建一个协程的成本是很低的。所以你会经常看到 Golang 开发的应用出现上千个协程并发的场景。

Golang协程池gopool怎么设计与实现

Goroutine 的优势:

  • 与线程相比,Goroutines 成本很低。

它们的堆栈大小只有几 kb,堆栈可以根据应用程序的需要增长和缩小,context switch 也很快,而在线程的情况下,堆栈大小必须指定并固定。

  • Goroutine 被多路复用到更少数量的 OS 线程。

一个包含数千个 Goroutine 的程序中可能只有一个线程。如果该线程中的任何 Goroutine 阻塞等待用户输入,则创建另一个 OS 线程并将剩余的 Goroutine 移动到新的 OS 线程。所有这些都由运行时处理,作为开发者无需耗费心力关心,这也使得我们有很干净的 API 来支持并发。

  • Goroutines 使用 channel 进行通信。

channel 的设计有效防止了在使用 Goroutine 访问共享内存时发生竞争条件(race conditions) 。channel 可以被认为是 Goroutine 进行通信的管道。

协程池

在高并发场景下,我们可能会启动大量的协程来处理业务逻辑。协程池是一种利用池化技术,复用对象,减少内存分配的频率以及协程创建开销,从而提高协程执行效率的技术。

最近抽空了解了字节官方开源的 gopkg 库提供的 gopool 协程池实现,感觉还是很高质量的,代码也非常简洁清晰,而且 Kitex 底层也在使用 gopool 来管理协程,这里我们梳理一下设计和实现。

gopool

了解官方 README 就会发现gopool的用法其实非常简单,将曾经我们经常使用的go func(){...}替换为gopool.Go(func(){...}) 即可。

此时 gopool 将会使用默认的配置来管理你启动的协程,你也可以选择针对业务场景配置池子大小,以及扩容上限。

old:

gofunc(){//doyourjob}()

new:

import("github.com/bytedance/gopkg/util/gopool")gopool.Go(func(){///doyourjob})

核心实现

下面我们来看看gopool是怎样实现协程池管理的。

Pool

Pool 是一个定义了协程池能力的接口。

typePoolinterface{//池子的名称Name()string//设置池子内Goroutine的容量SetCap(capint32)//执行f函数Go(ffunc())//带ctx,执行f函数CtxGo(ctxcontext.Context,ffunc())//设置发生panic时调用的函数SetPanicHandler(ffunc(context.Context,interface{}))}

gopool 提供了这个接口的默认实现(即下面即将介绍的pool),当我们直接调用 gopool.CtxGo 时依赖的就是这个。

这样的设计模式在 Kitex 中也经常出现,所有的依赖均设计为接口,便于随后扩展,底层提供一个默认的实现暴露出去,这样对调用方也很友好。

typepoolstruct{//池子名称namestring//池子的容量,即最大并发工作的goroutine的数量capint32//池子配置config*Config//task链表taskHead*tasktaskTail*tasktaskLocksync.MutextaskCountint32//记录当前正在运行的worker的数量workerCountint32//当worker出现panic时被调用panicHandlerfunc(context.Context,interface{})}//NewPool创建一个新的协程池,初始化名称,容量,配置funcNewPool(namestring,capint32,config*Config)Pool{p:=&pool{name:name,cap:cap,config:config,}returnp}

调用 NewPool 获取了以 Pool 的形式返回的 pool 结构体。

Task

typetaskstruct{ctxcontext.Contextffunc()next*task}

task 是一个链表结构,可以把它理解为一个待执行的任务,它包含了当前节点需要执行的函数f, 以及指向下一个task的指针。

综合前一节 pool 的定义,我们可以看到,一个协程池 pool 对应了一组task

pool 维护了指向链表的头尾的两个指针:taskHeadtaskTail,以及链表的长度taskCount 和对应的锁 taskLock

Worker

typeworkerstruct{pool*pool}

一个 worker 就是逻辑上的一个执行器,它唯一对应到一个协程池 pool。当一个worker被唤起,将会开启一个goroutine ,不断地从 pool 中的 task链表获取任务并执行。

func(w*worker)run(){gofunc(){for{//声明即将执行的taskvart*task//操作pool中的task链表,加锁w.pool.taskLock.Lock()ifw.pool.taskHead!=nil{//拿到taskHead准备执行t=w.pool.taskHead//更新链表的head以及数量w.pool.taskHead=w.pool.taskHead.nextatomic.AddInt32(&w.pool.taskCount,-1)}//如果前一步拿到的taskHead为空,说明无任务需要执行,清理后返回ift==nil{w.close()w.pool.taskLock.Unlock()w.Recycle()return}w.pool.taskLock.Unlock()//执行任务,针对panic会recover,并调用配置的handlerfunc(){deferfunc(){ifr:=recover();r!=nil{msg:=fmt.Sprintf("GOPOOL:panicinpool:%s:%v:%s",w.pool.name,r,debug.Stack())logger.CtxErrorf(t.ctx,msg)ifw.pool.panicHandler!=nil{w.pool.panicHandler(t.ctx,r)}}}()t.f()}()t.Recycle()}}()}

整体来看

看到这里,其实就能把整个流程串起来了。我们来看看对外的接口 CtxGo(context.Context, f func()) 到底做了什么?

funcGo(ffunc()){CtxGo(context.Background(),f)}funcCtxGo(ctxcontext.Context,ffunc()){defaultPool.CtxGo(ctx,f)}func(p*pool)CtxGo(ctxcontext.Context,ffunc()){//创建一个task对象,将ctx和待执行的函数赋值t:=taskPool.Get().(*task)t.ctx=ctxt.f=f//将task插入pool的链表的尾部,更新链表数量p.taskLock.Lock()ifp.taskHead==nil{p.taskHead=tp.taskTail=t}else{p.taskTail.next=tp.taskTail=t}p.taskLock.Unlock()atomic.AddInt32(&p.taskCount,1)//以下两个条件满足时,创建新的worker并唤起执行://1.task的数量超过了配置的限制//2.当前运行的worker数量小于上限(或无worker运行)if(atomic.LoadInt32(&p.taskCount)>=p.config.ScaleThreshold&&p.WorkerCount()<atomic.LoadInt32(&p.cap))||p.WorkerCount()==0{//worker数量+1p.incWorkerCount()//创建一个新的worker,并把当前pool赋值w:=workerPool.Get().(*worker)w.pool=p//唤起worker执行w.run()}}

相信看了代码注释,大家就能理解发生了什么。

gopool 会自行维护一个 defaultPool,这是一个默认的 pool 结构体,在引入包的时候就进行初始化。当我们直接调用 gopool.CtxGo() 时,本质上是调用了 defaultPool 的同名方法

funcinit(){defaultPool=NewPool("gopool.DefaultPool",10000,NewConfig())}const(defaultScalaThreshold=1)//Configisusedtoconfigpool.typeConfigstruct{//控制扩容的门槛,一旦待执行的task超过此值,且worker数量未达到上限,就开始启动新的workerScaleThresholdint32}//NewConfigcreatesadefaultConfig.funcNewConfig()*Config{c:=&Config{ScaleThreshold:defaultScalaThreshold,}returnc}

defaultPool 的名称为 gopool.DefaultPool,池子容量一万,扩容下限为 1。

当我们调用 CtxGo时,gopool 就会更新维护的任务链表,并且判断是否需要扩容 worker

  • 若此时已经有很多 worker 启动(底层一个 worker 对应一个 goroutine),不需要扩容,就直接返回。

  • 若判断需要扩容,就创建一个新的worker,并调用 worker.run()方法启动,各个worker会异步地检查 pool 里面的任务链表是否还有待执行的任务,如果有就执行。

三个角色的定位

  • task 是一个待执行的任务节点,同时还包含了指向下一个任务的指针,链表结构;

  • worker 是一个实际执行任务的执行器,它会异步启动一个 goroutine 执行协程池里面未执行的task

  • pool 是一个逻辑上的协程池,对应了一个task链表,同时负责维护task状态的更新,以及在需要的时候创建新的 worker

使用 sync.Pool 进行性能优化

其实到这个地方,gopool已经是一个代码简洁清晰的协程池库了,但是性能上显然有改进空间,所以gopool的作者应用了多次 sync.Pool 来池化对象的创建,复用woker和task对象。

这里建议大家直接看源码,其实在上面的代码中已经有所涉及。

  • task 池化

vartaskPoolsync.Poolfuncinit(){taskPool.New=newTask}funcnewTask()interface{}{return&task{}}func(t*task)Recycle(){t.zero()taskPool.Put(t)}

  • worker 池化

varworkerPoolsync.Poolfuncinit(){workerPool.New=newWorker}funcnewWorker()interface{}{return&worker{}}func(w*worker)Recycle(){w.zero()workerPool.Put(w)}

到此,关于“Golang协程池gopool怎么设计与实现”的学习就结束了,希望能够解决大家的疑惑。理论与实践的搭配能更好的帮助大家学习,快去试试吧!若想继续学习更多相关知识,请继续关注恰卡编程网网站,小编会继续努力为大家带来更多实用的文章!

发布于 2022-04-15 22:27:38
收藏
分享
海报
0 条评论
34
上一篇:微信小程序如何实现走马灯式抽奖 下一篇:Vue keep-alive的实现原理是什么
目录

    0 条评论

    本站已关闭游客评论,请登录或者注册后再评论吧~

    忘记密码?

    图形验证码