一文带你更方便的控制 goroutine

简介: 一文带你更方便的控制 goroutine

上一篇我们讲了 go-zero 中的并发工具包 core/syncx

从整体分析来看,并发组件主要通过 channel + mutex 控制程序中协程之间沟通。

Do not communicate by sharing memory; instead, share memory by communicating.

不要通过共享内存来通信,而应通过通信来共享内存。

本篇来聊 go-zero 对 Go 中 goroutine 支持的并发组件。

我们回顾一下,go原生支持的 goroutine 控制的工具有哪些?

  1. go func() 开启一个协程
  2. sync.WaitGroup 控制多个协程任务编排
  3. 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  就帮开发者减轻了负担,开发者只需要关注:

  1. 任务逻辑【函数】
  2. 任务数【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.Poolsync.Pool 有个不方便的地方是它池化的对象可能会被垃圾回收掉,这个就让开发者疑惑了,不知道自己创建并存入的对象什么时候就没了。

go-zero 中的 pool

  1. pool 中的对象会根据使用时间做懒销毁;
  2. 使用 cond 做对象消费和生产的通知以及阻塞;
  3. 开发者可以自定义自己的生产函数,销毁函数;

那我来看看生产对象,和消费对象在 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-zeroCond 的使用。可以类比 生产者-消费者模型,只是在这里没有使用 channel 做通信,而是用 Cond 。这里有几个特性:

  • Cond和一个Locker关联,可以利用这个Locker对相关的依赖条件更改提供保护。
  • Cond可以同时支持 SignalBroadcast 方法,而 Channel 只能同时支持其中一种。

总结

工具大于约定和文档,一直是 go-zero 设计主旨之一;也同时将平时业务沉淀到组件中,这才是框架和组件的意义。

关于 go-zero 更多的设计和实现文章,可以持续关注我们。

项目地址

https://github.com/tal-tech/go-zero

相关文章
你的应用进入了中断状态,但无任何代码显示,因为所有线程之前都在执行外部代码
你的应用进入了中断状态,但无任何代码显示,因为所有线程之前都在执行外部代码
2678 0
你的应用进入了中断状态,但无任何代码显示,因为所有线程之前都在执行外部代码
|
4月前
|
Java 程序员
Java多线程编程是指在一个进程中创建并运行多个线程,每个线程执行不同的任务,并行地工作,以达到提高效率的目的
【6月更文挑战第18天】Java多线程提升效率,通过synchronized关键字、Lock接口和原子变量实现同步互斥。synchronized控制共享资源访问,基于对象内置锁。Lock接口提供更灵活的锁管理,需手动解锁。原子变量类(如AtomicInteger)支持无锁的原子操作,减少性能影响。
44 3
|
3月前
|
Python
Python的`signal`模块提供了访问底层操作系统提供的信号机制的方式。信号是操作系统用来通知进程发生了某种情况(如用户按下Ctrl+C)的一种机制。
Python的`signal`模块提供了访问底层操作系统提供的信号机制的方式。信号是操作系统用来通知进程发生了某种情况(如用户按下Ctrl+C)的一种机制。
|
5月前
|
安全 Go
Go语言并发打印:通过通道实现协同输出
Go语言并发打印:通过通道实现协同输出
102 0
|
10月前
|
监控 安全 Windows
4.3 Windows驱动开发:监控进程与线程对象操作
在内核中,可以使用`ObRegisterCallbacks`这个内核回调函数来实现监控进程和线程对象操作。通过注册一个`OB_CALLBACK_REGISTRATION`回调结构体,可以指定所需的回调函数和回调的监控类型。这个回调结构体包含了回调函数和监控的对象类型,还有一个`Altitude`字段,用于指定回调函数的优先级。优先级越高的回调函数会先被调用,如果某个回调函数返回了一个非NULL值,后续的回调函数就不会被调用。当有进程或线程对象创建、删除、复制或重命名时,内核会调用注册的回调函数。回调函数可以访问被监控对象的信息,如句柄、进程ID等,并可以采取相应的操作,如打印日志、记录信息等。
80 0
4.3 Windows驱动开发:监控进程与线程对象操作
|
10月前
|
存储 前端开发 rax
协程切换的三种底层实现方式
协程切换的三种底层实现方式
102 0
|
芯片
查询方式的按键驱动程序
查询方式的按键驱动程序
129 0
|
存储 Python
Python语言如何在一个单独的线程中进行快速的IO操作
Python语言如何在一个单独的线程中进行快速的IO操作
119 0
Python语言如何在一个单独的线程中进行快速的IO操作
|
物联网 开发者
独立看门狗应用实例|学习笔记
快速学习独立看门狗应用实例
121 0
独立看门狗应用实例|学习笔记
|
监控 Windows
驱动开发:监控进程与线程对象操作
监控进程对象和线程对象操作,可以使用`ObRegisterCallbacks`这个内核回调函数,通过回调我们可以实现保护calc.exe进程不被关闭,具体操作从`OperationInformation->Object`获得进程或线程的对象,然后再回调中判断是否是计算器,如果是就直接去掉`TERMINATE_PROCESS`或`TERMINATE_THREAD`权限即可。
310 0
驱动开发:监控进程与线程对象操作