在go中,gorotine
是一种轻量级线程,由go运行时环境管理,相对于操作系统线程来说,创建和销毁gorotine
的开销非常小。通过利用goroutine
,可以处理大量并发任务而无需过多的系统资源。
但是如果大量创建使用rotoutine
,也会带来一些弊端。
- 内存消耗:每个
gorotine
都需要一定的内存来维持其状态,因为大量使用gorotine
可能会占用大量内存。 - 虽然
goroutine
的创建和销毁开销较小,但是对于大量的goroutine
,调度和管理这些并发任务也会带来一定的开销。 - 竞态条件:当多个
goroutine
同时访问共享资源时,可能会出现竞态条件(race condition),需要额外的同步机制来保证数据的正确性。 - 上下文切换:大量的
goroutine
可能会导致频繁的上下文切换,从而降低系统整体的性能。
因此,需要一个框架来管理、创建、调度、销毁这些goroutine
。ants就是这方面的优秀代表。
ants简介
引用ants官方介绍:github.com/panjf2000/a...
ants是一个高性能的 goroutine 池,实现了对大规模 goroutine 的调度管理、goroutine 复用,允许使用者在开发并发程序的时候限制 gorotine 数量,复用资源,达到更高效执行任务的效果。
ants功能
- 自动调度海量的 gotoutines, 复用 gorotines
- 定期清理过期的 gorotines,进一步节省资源。
- 提供了大量有用的接口:任务提交、获取运行中的 gotoutine数量、动态调整 Pool 大小、释放 Pool、重启 Pool
- 优雅处理 panic,防止程序崩溃
- 资源复用,极大节省内存使用量;在大规模批量并发任务场景下比原生 goroutine 并发具有更高的性能
- 非阻塞机制
ants流程图
简单示例
go
func demoFunc() {
time.Sleep(10 * time.Millisecond)
fmt.Println("Hello World!")
}
func main() {
defer ants.Release()
runTimes := 1000
// Use the common pool.
var wg sync.WaitGroup
syncCalculateSum := func() {
demoFunc()
wg.Done()
}
for i := 0; i < runTimes; i++ {
wg.Add(1)
_ = ants.Submit(syncCalculateSum)
}
wg.Wait()
fmt.Printf("running goroutines: %d\n", ants.Running())
fmt.Printf("finish all tasks.\n")
在上述代码中,使用ants.Submit
方法提交任务,协程池为默认的协程池。最后执行pool.Release()
方法释放协程池。
核心数据结构与方法
在本文章中只介绍一些核心的数据结构与方法,并不会详细介绍全部数据结构与方法。
第一个重要的核心数据结构就是 Pool
,能同时接收处理 goroutines ,并限制它们的数量并循环利用等功能。
通过调用 NewPool
方法来创建一个 Pool。
Pool创建好后,我们就可以提交任务交由它进行处理。
提交任务的入口为 Pool.Submit()
方法。
go
func (p *Pool) Submit(task func()) error {
if p.IsClosed() {
return ErrPoolClosed
}
w, err := p.retrieveWorker()
if w != nil {
w.inputFunc(task)
}
return err
}
- 当该 Pool 关闭时会返回异常。是否关闭是通过 state 变量进行判断的,0:开启,1: 关闭
- 调用
retrieveWorker()
方法获取一个可用的worker - 将用户定义的任务提交到该 worker 的 channel 中,具体实现方式就是下述代码
go
func (w *goWorker) inputFunc(fn func()) {
w.task <- fn
}
介绍到此处,就会发现 worker 是一个十分重要的数据结构,用户提交的任务最终交给它进行处理。
我们可以把它理解为一个长时间运行而不回收的协程,用于反复处理用户提交的异步任务。
简单介绍完了 worker 的数据结构及用处,那它是怎么获取的呢?
获取的方法就是上处的 retrieveWorker()
方法。
整体逻辑如下:
- 加锁,尝试从 workerQueue 工作池中获取,如果获取到,直接解锁并返回
- 如果运行中的worker数量没有超过工作池容量,解锁,从对象池 workerCache 中获取一个worker, 调用 run 方法使其运行,并返回
- 如果协程池是非阻塞模式,或者阻塞数量超过设定的最大数量,解锁并返回异常
- 如果协程是阻塞模式,waiting 变量值加一,并调用 cond.Wait() 方法进行阻塞等待,等待有可用的 worker, 当被唤醒后, waiting 变量减一,并重新执行上述逻辑进行获取。
从上面我们可以发现,获取 worker 的途径有两个,一个是 workerQueue 工作池,一个是 workerCache 对象池。
workerQueue工作池的 worker 是什么时候被放入进来的呢,通过最上面的整体流程图,我们就可以发现,当一个 worker 执行完用户定义的任务后,就会被放入到 workerQueue 工作池中。
所以,要想解决上述的疑问,首先要先了解 worker 是怎么运行的。
worker 是执行 run 方法进行启动的。
这里我们只是知道 worker 是怎么运行和放入到工作池 workerQueue 的,那它是如何产生的呢?
实际 worker 的产生是在 workerCache 对象池中, sync.Pool 的具体机制,大家可以搜索一下,在这里就不会详细介绍了。
sync.Pool 用于存储临时对象以供重复使用,从而降低内存分配的开销。通过避免频繁地分配新对象,可以提高程序的性能。sync.Pool 可以在需要时分配对象,并在不再需要时释放这些对象。它适用于对一组对象进行频繁的读取和写入操作的情况,例如在连接池、临时对象池等方面有着广泛的应用。
最多两轮 gc,pool 内的对象资源将会全被回收
当调用 Get 方法进行获取时,就会执行 New 方法新建一个对象并返回,其 New 方法的具体实现在协程池中的 NewPool 方法中定义好了,具体为:
go
p.workerCache.New = func() interface{} {
return &goWorker{
pool: p,
task: make(chan func(), workerChanCap),
}
}
至此,有关协程池是怎么创建并执行任务的大致流程就介绍完毕了,实际还有一些细节没有介绍,比如,如何回收过期的 worker 的,这些细节介绍,大家可以搜索一些其他博客。