Golang协程池(goroutine pool)

64 阅读1分钟

如果Golang的协程(goroutine)频繁地创建和销毁,将会消耗很多CPU资源。通过实现groutine池,实现协程复用,提高效率。

  1. 创建工作者Worker结构体,worker会开辟一个协程,并从通道ch中接受任务,并运行
type Worker struct {
    id int
    ch chan func()
}
  1. 创建线程池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))
}
  1. 线程池注意事项

    • 上图分配任务给worker时采用随机分配,这种效果不好,可以考虑使用轮询或其他负载均衡的方法。
    • 创建协程池pool的size不好确定。size大了,空闲的worker多,浪费资源,size小了又达不到并发效果。
    • 确定任务的瓶颈,如果瓶颈是堵塞式磁盘IO, 采用协程池效果不大。如果任务瓶颈是CPU资源,则应该采用协程池。