协程池
每次任务的执行,都去go一个新的协程,这其中会可能会牵扯到,协程的创建分配调度等一些列流程,可能对我们的 内存和cpu利用率都不友好,为了减少资源的浪费,我们可以将 协程 进行 池** 化,复用协程, 减少资源过度浪费
一个协程应该具备那些基本的功能
- 执行协程的 panic处理
- 自定义协程池的大小
- 协程任务的等待执行
参考链接
go-playground/pool.v3
一个功能简单易用的协程池,var poolWorks = pool.NewLimited(num)
, 当创建了 限制limt了后,每次业务处理最多只有 num个work的goroutine在 并发处理。
功能
- 支持unlimt 和 limit的创建协程池
- 支持协程的cancel
- 协程recover的处理
- 隐藏处理细节,使用简单,固定套路和流程 Batch->Queue->QueueComplete->Results
- 协程的创建是 "饿汉模式", 程序在初始化的就创建好的协程了
执行流程
0、在初始化协程池时候会异步创建 newWorker
来处理 任务
0、创建batch对象,将task到 queue时,都是异步添加
0、等待并阻塞这直到所有的任务完成
Demo
var worker = pool.NewLimited(2)
func main() {
// defer worker.close()
batch := worker.Batch()
batch.Queue(func(wu pool.WorkUnit) (interface{}, error) { // 往队列里面写入是异步操作
getUserInfo()
return "Queue-getUserInfo", nil
})
batch.Queue(func(wu pool.WorkUnit) (interface{}, error) {
getTeacher()
return "Queue-getTeacher", nil
})
// 告诉batch 没有要执行的 func, 当work执行完毕 close(result)避免一直阻塞
batch.QueueComplete()
// 等待所有的 Queue 函数执行完毕,并获取返回值
for ch := range batch.Results() {
fmt.Println(ch.Value())
}
}
func getUserInfo() {
time.Sleep(1 * time.Second)
fmt.Println("getUserInfo")
}
func getTeacher() {
time.Sleep(5 * time.Second)
fmt.Println("getTeacher")
}
ants
功能
- 自动调度海量的 goroutines,复用 goroutines
- 定期清理过期的 goroutines,进一步节省资源
- 提供了大量有用的接口:任务提交、获取运行中的 goroutine 数量、动态调整 Pool 大小、释放 Pool、重启 Pool
- 优雅处理 panic,防止程序崩溃
- 资源复用,极大节省内存使用量;在大规模批量并发任务场景下比原生 goroutine 并发具有更高的性能
- 非阻塞机制
- 协程的创建是 “懒汉模式”,需要的时候就创建,
执行流程
0、从流程图我们能发现,在初始化 工作池时,会启动一个goroutine来定时清理 过期worker,从而节省资源
0、每次worker执行完任务后,都会放到 workerArray 中,循环队列尾部/栈底,有新的任务则会从 循环队列头部/栈顶获取可执行worker
0、当执行任务 发生panic 我们也会 将 gowork
对象 放到 对象池方便一些复用对象。
DEMO
- 使用默认的 defaultAntsPool
适合执行不同的 任务task
func Test_dft(t *testing.T) {
defer ants.Release()
runTimes := 1000
var wg sync.WaitGroup
syncCalculateSum := func() {
fmt.Print("hello world")
wg.Done()
}
for i := 0; i < runTimes; i++ {
wg.Add(1)
ants.Submit(syncCalculateSum)
}
wg.Wait()
fmt.Printf("running goroutines: %d\n", ants.Running()) // 可能会减1
fmt.Printf("finish all tasks.\n")
}
- 使用 池函数
适合一批goroutine执行 同一个任务函数,
var sum int32
// ants的方式,
func Test_func(t *testing.T) {
var wg sync.WaitGroup
pf, _ := ants.NewPoolWithFunc(10, func(i interface{}) {
val, _ := i.(int32)
atomic.AddInt32(&sum, val)
wg.Done()
})
defer pf.Release()
runTimes := 1000
for i := 0; i < runTimes; i++ {
wg.Add(1)
pf.Invoke(int32(i))
}
wg.Wait()
fmt.Printf("running goroutines: %d\n", pf.Running())
fmt.Printf("finish all tasks, result is %d\n", sum)
}
- opetions参数
type Options struct {
// worker的过期时间,默认1s
ExpiryDuration time.Duration
// 是否要预分配对象,这个分配的对象是 workerArray【statk,queue】
PreAlloc bool
// 最大要阻塞的任务数量,默认不限制0
MaxBlockingTasks int
// 不阻塞等待任务执行
Nonblocking bool
// 自定义panicHandker处理函数
PanicHandler func(interface{})
// 自定义logger的处理
Logger Logger
}
VS
- 从功能上
功能 | Pool.v3 | ants |
---|---|---|
panic自定义处理函数 | ❌ | ✅ |
协程错误recover处理 | ✅ | ✅ |
定期清理过期woker | ❌ | ✅ |
动态修改协程池子大小 | ❌ | ✅ |
对象创建复用 | ❌ | ✅ |
- 从设计上
ants 使用 对象池sync.pool 和 定时器 来减少对象分配和 资源的浪费,可以指定 等待完成的任务数。
go-playground/pool.v3 就是简单生产者消费者,没有考虑 复用,每个task的任务的执行,强制性的等待完成才行。
- 代码测试
网上一大堆就不举例了
总结
- 优先选择 ant,因为ant的功能丰富,设计理念做到了 资源利用的最大化。自定义等待完成的任务数。worker数量动态扩增与缩减
- ant任务的执行一定是 submit-> 获取可执行的goworker ->执行task -》 放到workerarray等待下一次的任务执行
- ant中用的了sync.cond 来实现 协程的广播通知 和 通知某个正在阻塞等待协程
Q&A
ant-goroutine的goWorker-life
- goroutine-task发生panic
- 从goroutine-task中取出 nil(过期了)
- workerarray没有空闲的free-wroker
ant-pool的容量可以为-1的
- 在初始化pool是如何设置为 size 小于等0则 设置为-1
- 如果你设置了预分配workerArray,PreAlloc=true,则设置-1则提现错误
ant-为什么高效
- 在初始化时候 启动一个goroutine,会启动一个goroutine来定时清理 过期worker,从而节省资源
- 每次创建的goWorker对象是从 对象池创建,保证了对象的复用
ants-用栈和用循环队列区别
- 使用
loopQueueType
循环队列则对容量又限制 - 使用
stackType
栈则对容量没限制
ants-Running值的统计
- 当有新的
goWorker
被被run的的使用 +1 - 当
goWorker
执行task,发生painc 或者 从task中读取为nil值(过期了)或者 没有剩余的workerarray空间时候会 running -1
获取过期的worker
nowTime=10点
expireTime = 1h
workerarray【worker9-7点,worker8-8点,worker7-9点,worker6-10点】
因为该 workeryarray是有序的,所以是用二分查找发
找到过期的 worker的分界线
stackType
0、设置过期的 expire worerks
0、删除过期的expire workers从 items中, iterms[i] = nil
0、重新调整 items
ant-从workerCache中获取对象不需要reset
type goWorker struct {
// pool who owns this worker.
pool *Pool
// task is a job should be done.
task chan func()
// recycleTime will be updated when putting a worker back into queue.
recycleTime time.Time
}
0、每个goWorker都共享这个一个 pool,所以不能reset
0、goWorker.task 是一个 make(chan func,1),当从task中取出数据后,task里面就是无数据的了,所以不需要reset
0、每个submit的任务的提交,都会尝试从 wokerarray中获取下一个可执行的 worker,
0、执行完task 后 每次都会 revertWorker 放进 wokerarray 中 来重新设置该值,
业务中如何合理设置池大小
方案一【不限制池子大小】
如果对机器的性能没有什么要求则,则可以直接采用 不限制池的大小的方法来使用协程池,因为ants本身也提供了,【定期删除 不适用worker】,所以也不需要担心的资源的浪费
方案二【限制池子大小】
对资源使用的更加精细化的分配使用,可以使用一下思路来设置我们的池子大小
- 根据监控统计该接口qps,根据qps可是设置一个 初始值
- 在代码中设置动态库容机制
// 方法1,启动一个定时器,定期监测判断是否需要扩容,
// 方法2,包装 ants,新增一个 自定义动态扩容函数,然后启动定时器,不间断的执行用户自己定义的扩容函数
func DynamicResize() {
// 等待的数量已经超过我本身容量的 1/4了则触发扩容机制
// 可能我的自己的设置的 size不合适
if antsPool3.Waiting() > antsPool3.Cap()/4 {
antsPool3.Tune(antsPool3.Waiting()/4 + antsPool3.Cap())
}
}
- 将 Running 的数量的也进行上报,后期也可以参考 running的均值,来重新设置 池子的 cap