逍遥游(1): ants是如何实现一个协程池的

2,249 阅读3分钟

概要

Go的协程非常轻量,但是在超高并发场景,每个请求创建一个协程也是低效的,一个简单的思想就是协程池。另一种路径为事件驱动,例如字节跳动的HTTP框架Hertz。它使用字节跳动自研高性能网络库 Netpoll,可以简单理解为网络编程中常用的I/O多路复用,例如select和epoll。

需求分析

阅读源码也不能忽略独立思考和需求分析,也就是如果需要独立实现一个协程池,我们需要实现什么功能?实现的方式是什么?这是最基础的思考。

协程池的需求

  • 支持提交一个任务,例如func()函数
  • 控制协程的数量,任务提交时如果没有空闲协程则阻塞
  • 协程池的状态管理
  • 协程池不会发生协程泄露
  • 最后是高性能,尽可能降低并发性能开销

设计分析

如何提交任务

  • Go的函数是一等公民,所以一个任务使用一个函数进行抽象即可,最通用的函数是func()。
  • 但是如何提交任务呢?如果是其他语言例如Java,会选择使用线程安全的Queue容器,但是在Go以及该场景下,使用channel无疑是更好的选择 ants也是这么想的,简化版的worker(接口类型)如下:
type goWorker struct {
    task chan func()
}

func (w *goWorker) run() {
    for f := range w.task {
        f()
    }
}

如何控制协程的数量

不可避免,我们要实现一个存储worker数组的一种数据结构,假设叫它queue,它至少需要支持

  • pop() worker
  • push(worker)
  • close() ants也提供了2种实现:
  1. workerStack
  2. loopQueue

细节1:当协程不够用了如何阻塞和唤醒?

熟悉并发编程的肯定先想到的是条件变量,ants也是使用的条件变量,当然还有配套的Mutex

{
    lock sync.Locker
    cond *sync.Cond
}

因此,ants是由pool来控制协程数量的,对应于一个原子int32 capacity,queue只负责管理worker的push和pop。

协程池的状态管理

这个其实知道了也不难,用一个原子int即可,int表示状态,原子保护并发竞争。

协程池不会发生协程泄露

协程泄露算是Go特有的程序错误了,发生的概率还不低,一般我们使用一个stopCh控制协程的关闭。 ants不这样,它通过传入一个nil给taskCh,让goWorker结束运行。

高性能,尽可能降低并发性能开销

在上面的分析中,我们已经尽可能减少使用全局锁了,例如用原子变量,用条件变量。 除此之外,ants还是要了sync.Pool创建worker,因为ants会定期停止空闲时间过长的worker,然后销毁掉这些worker,避免协程阻塞空转。

数据结构设计

通过以上分析,我们可以设计出初步的数据结构,开始编码了

worker

type goWorker struct {
    // pool 用于把worker放回queue
    pool *pool

    task chan func()
    close chan struct{}
}

queue

type workerStack struct {
    items  []*goWorker
    size   int
}

pool

type Pool struct {
    capacity int32
    running int32
    blockingNum int
    lock sync.Locker
    cond *sync.Cond
    workers workerArray
    state int32
}

参考链接

  1. github.com/panjf2000/a…
  2. strikefreedom.top/archives/hi…