如果Golang的协程(goroutine)频繁地创建和销毁,将会消耗很多CPU资源。通过实现groutine池,实现协程复用,提高效率。
- 创建工作者
Worker结构体,worker会开辟一个协程,并从通道ch中接受任务,并运行
type Worker struct {
id int
ch chan func()
}
- 创建线程池
Pool结构体,Pool管理worker,接受任务并分配给worker
package main
import (
"fmt"
"math/rand"
"sync"
"time"
)
type Pool {
workers []*Worker
}
// 新建一个拥有size个worker的线程池Pool
func NewPool(size int) *Pool {
pool := &Pool{
workers: make([]*Worker, size)
}
// 激活worker, 每个worker开辟一个gorouine,等待ch通道推送任务func()
for i := 0; i < size; i++ {
worker := &Worker{
id:i,
// 设置一个有缓存的ch,无堵塞地接受任务func()
ch: make(chan func(), 10),
}
// 开辟goroutine, 监听ch
go func(worker *Worker) {
for fn := range worker.ch {
fn()
}
}(worker)
// 将worker添加到Pool
pool[i] = worker
}
return pool
}
// 将任务提交给pool, pool随机分配给worker
func (p *Pool) Submit(fn func()) {
worker := p.workers[rand.Intn(len(p.workers))]
worker.ch <- fn
}
func (p *Pool) Close() {
for _, w := range p.workers {
close(w.ch)
}
}
func main() {
// 创建10个工作者的线程
pool := NewPool(10)
// 最后释放goroutine
defer p.Close()
var wg sync.WaitGroup
t := time.Now()
// 将1000个任务发送到pool
for i := 0; i < 100; i++ {
wg.Add(1)
pool.Submin(func() {
fmt.Printf("handling in task %d", i)
// 模拟耗时
time.Sleep(1 * time.Second)
wg.Done()
})
}
// 等待所有任务完成
wg.wait()
// 耗时
fmt.Println(time.Since(t))
}
-
线程池注意事项
- 上图分配任务给worker时采用随机分配,这种效果不好,可以考虑使用轮询或其他负载均衡的方法。
- 创建协程池pool的size不好确定。size大了,空闲的worker多,浪费资源,size小了又达不到并发效果。
- 确定任务的瓶颈,如果瓶颈是堵塞式磁盘IO, 采用协程池效果不大。如果任务瓶颈是CPU资源,则应该采用协程池。