概要
Go的协程非常轻量,但是在超高并发场景,每个请求创建一个协程也是低效的,一个简单的思想就是协程池。另一种路径为事件驱动,例如字节跳动的HTTP框架Hertz。它使用字节跳动自研高性能网络库 Netpoll,可以简单理解为网络编程中常用的I/O多路复用,例如select和epoll。
需求分析
阅读源码也不能忽略独立思考和需求分析,也就是如果需要独立实现一个协程池,我们需要实现什么功能?实现的方式是什么?这是最基础的思考。
协程池的需求
- 支持提交一个任务,例如func()函数
- 控制协程的数量,任务提交时如果没有空闲协程则阻塞
- 协程池的状态管理
- 协程池不会发生协程泄露
- 最后是高性能,尽可能降低并发性能开销
设计分析
如何提交任务
- Go的函数是一等公民,所以一个任务使用一个函数进行抽象即可,最通用的函数是func()。
- 但是如何提交任务呢?如果是其他语言,无疑只能使用共享内存通信,但是在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种实现:
- workerStack
- 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
}