日常开发中,应对大量任务的时候,为了效率和性能,有时会用到线程池,在 golang 中,我们知道可以使用 用户态 线程,也就是协程来完成一定的功能,资源消耗更少,性能开销少。
通常如果任务不复杂的话,我们完成可以通过 go func() 来开启协程异步执行,但是遇到更复杂的情况时,我们还是更倾向于协程池的使用,就好比单个数据库的连接,直接建立连接用就是了,如果连接数多,每次都要重建连接归还连接,费时效率还不高,这个时候就需要连接池上场了。
池化的思想就是省去频繁创建或者销毁的资源消耗,让这些工作交给特定的 xxx池 来完成,需要的时候,直接用就好了。
比如我们运行一个多任务的场景,限定一定数量额 worker ,如果让自己来设计,起码应该有以下要考虑的点:
- 1.生产者-消费者模型,生产者产生任务,消费者消费任务,或者是 task-worker 模型,思想都是一样的。
- 2.生产者与消费者在数据传递上需要相应的机制,比如 golang 语言中可以使用 chan 进行任务的传递。
- 3.消费者也就是 worker ,既然是池化了,就必须有 worker 的新建与任务到达时的取用,用完再回收。
- 4.协程池除了创建外,在任务被执行完毕后,也应该回收资源,关闭 chan 等
- 5.协程池的设计,应该尽量是可配置的,比如运行 worker 的数量设置,如果任务多且耗时,worker 数量少,必然导致运行时间长,且需要考虑当任务数量大于 worker 数量时,暂未被处理的任务,协程池的策略是什么,阻塞等待空闲 worker 或者是认为超出协程池的处理能力,直接就 drop 了呢。
- 6.如果某个 worker 在处理任务时发生异常,如 panic,常见的索引超范围等异常,应该如何处理,肯定不能直接导致协程池都不能用了,其他协程的运行应该不受影响,直到任务全部执行结束。
以上只是个人的一些思考,恰好在 golang 中有个强大的 ants协程池 库,其性能与功能都很优秀,所以就避免了自己造轮子各种 bug 的问题。
以下我们就 ants 的使用做一些示例说明。
首先下载 ants:
go get github.com/panjf2000/ants/v2
接下来开始 ants 的使用之旅吧。
1.不用新建协程池,直接用
ants库 很奈斯,知道你比较懒,所以你拿来即用,只是用完以后记得释放资源就是。
我们就运行2个简单任务:
func runWithPoolInDefault() {
defer ants.Release()
var wg sync.WaitGroup
wg.Add(2)
for i := 0; i < 2; i++ {
ants.Submit(blankTask)
wg.Done()
}
wg.Wait()
time.Sleep(time.Second)
fmt.Printf("runWithPoolInDefault pool run finished!\n")
}
func blankTask() {
fmt.Printf("exec blank task, time: %v\n", time.Now().Format(time.RFC3339Nano))
}
结果:
exec blank task, time: 2024-03-15T16:44:21.004938+08:00
exec blank task, time: 2024-03-15T16:44:21.004938+08:00
runWithPoolInDefault pool run finished!
从上面也可以看出来,真的很简单,直接用。
业务场景肯定更复杂,比如我们需要在任务中传入参数呢,我们就有下面的使用:
func runWithPoolInDefault() {
defer ants.Release()
var wg sync.WaitGroup
wg.Add(2)
for i := 0; i < 2; i++ {
ants.Submit(wrapper(i, &wg))
}
wg.Wait()
time.Sleep(time.Second)
fmt.Printf("runWithPoolInDefault pool run finished!\n")
}
func wrapper(idx int, wg *sync.WaitGroup) func() {
return func() {
fmt.Printf("exec task: %d, time: %v\n", idx+1, time.Now().Format(time.RFC3339Nano))
// ensure wg wont block
wg.Done()
}
}
可以看到,我们通过引入 包装函数+闭包 思想,将 ants 接口要求的传入无入参函数作为任务函数就转化为可传参数的任务,复杂任务可以参考包装函数传入需要的参数。