上一篇咱们讲了 go-zero
中的并发工具包 core/syncx
。
从整体剖析来看,并发组件次要通过 channel + mutex
控制程序中协程之间沟通。
Do not communicate by sharing memory; instead, share memory by communicating.
不要通过共享内存来通信,而应通过通信来共享内存。
本篇来聊 go-zero
对 Go 中 goroutine
反对的并发组件。
咱们回顾一下,go原生反对的 goroutine
管制的工具有哪些?
go func()
开启一个协程sync.WaitGroup
管制多个协程工作编排sync.Cond
协程唤醒或者是协程期待
那可能会问 go-zero
为什么还要拿出来讲这些?回到 go-zero
的设计理念:工具大于约定和文档。
那么就来看看,go-zero
提供哪些工具?
threading
尽管 go func()
曾经很不便,然而有几个问题:
- 如果协程异样退出,无奈追踪异样栈
- 某个异样申请触发panic,应该做故障隔离,而不是整个过程退出,容易被攻打
咱们看看 core/threading
包提供了哪些额定抉择:
func GoSafe(fn func()) { go RunSafe(fn)}func RunSafe(fn func()) { defer rescue.Recover() fn()}func Recover(cleanups ...func()) { for _, cleanup := range cleanups { cleanup() } if p := recover(); p != nil { logx.ErrorStack(p) }}
GoSafe
threading.GoSafe()
就帮你解决了这个问题。开发者能够将本人在协程中须要实现逻辑,以闭包的形式传入,由 GoSafe()
外部 go func()
;
当开发者的函数出现异常退出时,会在 Recover()
中打印异样栈,以便让开发者更快确定异样产生点和调用栈。
NewWorkerGroup
咱们再看第二个:WaitGroup
。日常开发,其实 WaitGroup
没什么好说的,你须要 N
个协程合作 :wg.Add(N)
,期待全副协程实现工作:wg.Wait()
,同时实现一个工作须要手动 wg.Done()
。
能够看的进去,在工作开始 -> 完结 -> 期待,整个过程须要开发者关注工作的状态而后手动批改状态。
NewWorkerGroup
就帮开发者加重了累赘,开发者只须要关注:
- 工作逻辑【函数】
- 工作数【
workers
】
而后启动 WorkerGroup.Start()
,对应工作数就会启动:
func (wg WorkerGroup) Start() { // 包装了sync.WaitGroup group := NewRoutineGroup() for i := 0; i < wg.workers; i++ { // 外部保护了 wg.Add(1) wg.Done() // 同时也是 goroutine 平安模式下进行的 group.RunSafe(wg.job) } group.Wait()}
worker
的状态会主动治理,能够用来固定数量的 worker
来解决音讯队列的工作,用法如下:
func main() { group := NewWorkerGroup(func() { // process tasks }, runtime.NumCPU()) group.Start()}
Pool
这里的 Pool
不是 sync.Pool
。sync.Pool
有个不不便的中央是它池化的对象可能会被垃圾回收掉,这个就让开发者纳闷了,不晓得本人创立并存入的对象什么时候就没了。
go-zero
中的 pool
:
pool
中的对象会依据应用工夫做懒销毁;- 应用
cond
做对象生产和生产的告诉以及阻塞; - 开发者能够自定义本人的生产函数,销毁函数;
那我来看看生产对象,和生产对象在 pool
中时怎么实现的:
func (p *Pool) Get() interface{} { // 调用 cond.Wait 时必须要持有c.L的锁 p.lock.Lock() defer p.lock.Unlock() for { // 1. pool中对象池是一个用链表连贯的nodelist if p.head != nil { head := p.head p.head = head.next // 1.1 如果以后节点:以后工夫 >= 上次应用工夫+对象最大存活工夫 if p.maxAge > 0 && head.lastUsed+p.maxAge < timex.Now() { p.created-- // 阐明以后节点曾经过期了 -> 销毁节点对应的对象,而后持续寻找下一个节点 // 【⚠️:不是销毁节点,而是销毁节点对应的对象】 p.destroy(head.item) continue } else { return head.item } } // 2. 对象池是懒加载的,get的时候才去创建对象链表 if p.created < p.limit { p.created++ // 由开发者本人传入:生产函数 return p.create() } p.cond.Wait() }}
func (p *Pool) Put(x interface{}) { if x == nil { return } // 互斥拜访 pool 中nodelist p.lock.Lock() defer p.lock.Unlock() p.head = &node{ item: x, next: p.head, lastUsed: timex.Now(), } // 放入head,告诉其余正在get的协程【极为要害】 p.cond.Signal()}
上述就是 go-zero
对 Cond
的应用。能够类比 生产者-消费者模型,只是在这里没有应用 channel
做通信,而是用 Cond
。这里有几个个性:
- Cond和一个Locker关联,能够利用这个Locker对相干的依赖条件更改提供爱护。
- Cond能够同时反对
Signal
和Broadcast
办法,而Channel
只能同时反对其中一种。
总结
工具大于约定和文档,始终是 go-zero
设计宗旨之一;也同时将平时业务积淀到组件中,这才是框架和组件的意义。
对于 go-zero
更多的设计和实现文章,能够继续关注咱们。欢送大家去关注和应用。
我的项目地址
https://github.com/tal-tech/go-zero
欢送应用 go-zero 并 star 反对咱们!
微信交换群
关注『微服务实际』公众号并回复 进群 获取社区群二维码。
go-zero 系列文章见『微服务实际』公众号