协程池的设计及代码

协程池的设计

前言:

尽管go官方宣称用golang写并发程序的时候随便起个成千上万的goroutine毫无压力;但是每个协程都会分配最小2k的内存,根据需要进行扩容,最大1G,哪怕最小的2k,当协程数达到一定数量时,内存会暴涨,所带来的就是gc的压力,频繁地进行gc也会给性能带来影响;此外内存暴涨,造成无内存可用时,Go调度器就会阻塞goroutine,结果就是P(GMP调度模型中的Processor逻辑处理器)的Local队列积压,又导致内存溢出,这就是个死循环…,甚至极有可能程序直接Crash掉;所以在大规模需要并发操作时,协程池是非常有必要的。

协程池的设计思路

初始化一个pool,这个pool中需要维护一个类似栈的LIFO队列 ,用来存放空闲的worker,,每个worker,接收一个tast进行协程操作,此外给每个worker设置了老化时间,方便定期回收过期的worker,通过定时清理过期worker,可以进一步节省系统资源;对于没有过期的worker会回收复用,因为复用,规避了无脑启动大规模goroutine的弊端,可以节省大量的内存。
当一个client端提交task到Pool中之后,在Pool内部,接收task之后的核心操作是:

  1. 检查空闲worker队列中是否有可用的worker,有的话,拿出最近执行任务的那个worker,即从队列的末尾拿最后一个
  2. 当没有空闲的worker时,判断当前正在running的worker数量是否已经达到pool容量,没有的话,新建一个新的worker;已经达到的话阻塞等待,等待一个新的worker被回收;
  3. 每个worker完成任务后,回收到空闲的worker队列中,并更新入队时间,按照执行时间的先后,放入到队列中;
  4. 定时任务来扫描空闲的worker队列中的最近的完成任务时间与当前时间之差是否大于定义的老化时间,是的话,释放掉该worker,节省内存资源;

代码

任务方法和信号

//空结构体不占内存,用于信号传递
type sig struct{}

type f func() error

pool结构体定义

// Pool accept the tasks from client,it limits the total
// of goroutines to a given number by recycling goroutines.
type Pool struct {
	// capacity of the pool.
	capacity int32

	// running is the number of the currently running goroutines.
	running int32

	// expiryDuration set the expired time (second) of every worker.
	//在空闲队列中的worker的最新一次运行时间与当前时间之差如果大于这个值则表示已过期,定时清理任务会清理掉这个worker
	expiryDuration time.Duration

	// freeSignal is used to notice pool there are available
	// workers which can be sent to work.
	//freeSignal是一个信号,因为Pool开启的worker数量有上限,因此当全部worker都在执行任务的时候,新进来的请求就需要阻塞等待,那当执行完任务的
	//worker被放回Pool之时,如何通知阻塞的请求绑定一个空闲的worker运行呢?freeSignal就是来做这个事情的
	freeSignal chan sig

	// workers is a slice that store the available workers.
	workers []*Worker

	// release is used to notice the pool to closed itself.
	release chan sig

	// lock for synchronous operation
	lock sync.Mutex

	once sync.Once
}

worker结构体定义

// Worker is the actual executor who runs the tasks,
// it starts a goroutine that accepts tasks and
// performs function calls.
type Worker struct {
	// pool who owns this worker.
	pool *Pool

	// task is a job should be done.
	task chan f

	// recycleTime will be update when putting a worker back into queue.
	recycleTime time.Time
}

初始化pool

// NewPool generates a instance of ants pool
func NewPool(size, expiry int) (*Pool, error) {
	if size <= 0 {
		return nil, ErrPoolSizeInvalid
	}
	p := &Pool{
		capacity:       int32(size),
		freeSignal:     make(chan sig, math.MaxInt32),
		release:        make(chan sig, 1),
		expiryDuration: time.Duration(expiry) * time.Second,
	}
	// 启动定期清理过期worker任务,独立goroutine运行,
	// 进一步节省系统资源
	p.monitorAndClear()
	return p, nil
}

清理空闲的worker协程

//定时清理过期的worker
func (p *Pool) monitorAndClear() {
	go func() {
		for {
			// 周期性循环检查过期worker并清理
			time.Sleep(p.expiryDuration)
			currentTime := time.Now()
			p.lock.Lock()
			idleWorkers := p.workers
			n := 0
			for i, w := range idleWorkers {
				// 计算当前时间减去该worker的最后运行时间之差是否符合过期时长
				if currentTime.Sub(w.recycleTime) <= p.expiryDuration {
					//因为采用了LIFO后进先出队列存放空闲worker,所以该队列默认已经是按照worker的最后运行时间由远及近排序
					break
				}
				n = i
				w.stop()
				//指针指向nil,主动释放堆中资源,不用go的gc来回收,减轻gc的压力
				idleWorkers[i] = nil
				p.running--
			}
			//重新将空闲的worker放入到pool的worker队列中
			if n > 0 {
				n++
				p.workers = idleWorkers[n:]
			}
			p.lock.Unlock()
		}
	}()
}

// stop this worker.
func (w *Worker) stop() {
	w.sendTask(nil)
}

提交任务

// Submit submit a task to pool
func (p *Pool) Submit(task f) error {
	if len(p.release) > 0 {
		return ErrPoolClosed
	}
	w := p.getWorker()
	w.sendTask(task)
	return nil
}

// sendTask sends a task to this worker.
func (w *Worker) sendTask(task f) {
	w.task <- task
}

获取空闲的worker

// getWorker returns a available worker to run the tasks.
func (p *Pool) getWorker() *Worker {
	var w *Worker
	// 标志,表示当前运行的worker数量是否已达容量上限
	waiting := false
	// 涉及从workers队列取可用worker,需要加锁
	p.lock.Lock()
	workers := p.workers
	n := len(workers) - 1
	// 当前worker队列为空(无空闲worker)
	if n < 0 {
		// 运行worker数目已达到该Pool的容量上限,置等待标志
		if p.running >= p.capacity {
			waiting = true
		} else {
			// 否则,运行数目加1
			p.running++
		}
	} else {
		// 有空闲worker,从队列尾部取出一个使用
		<-p.freeSignal
		w = workers[n]
		//主动回收堆中的资源
		workers[n] = nil
		p.workers = workers[:n]
	}
	// 判断是否有worker可用结束,解锁
	p.lock.Unlock()

	if waiting {
		// 阻塞等待直到有空闲worker
		<-p.freeSignal
		p.lock.Lock()
		workers = p.workers
		l := len(workers) - 1
		w = workers[l]
		workers[l] = nil
		p.workers = workers[:l]
		p.lock.Unlock()
	} else if w == nil {
		// 当前无空闲worker但是Pool还没有满,
		// 则可以直接新开一个worker执行任务
		w = &Worker{
			pool: p,
			task: make(chan f),
		}
		w.run()
	}
	return w
}

worker的run方法

func (w *Worker) run() {
	//atomic.AddInt32(&w.pool.running, 1)
	go func() {
		//监听任务列表,一旦有任务立马取出运行
		for f := range w.task {
			if f == nil {
				atomic.AddInt32(&w.pool.running, -1)
				return
			}
			f()

			//回收复用
			w.pool.putWorker(w)
		}
	}()
}

回收worker

// putWorker puts a worker back into free pool, recycling the goroutines.
func (p *Pool) putWorker(worker *Worker) {
	// 写入回收时间,亦即该worker的最后运行时间
	worker.recycleTime = time.Now()
	p.lock.Lock()
	p.workers = append(p.workers, worker)
	p.lock.Unlock()
	p.freeSignal <- sig{}
}

pool扩缩容

func (p *Pool) Cap() int {
	return int(p.capacity)

}

// ReSize change the capacity of this pool
func (p *Pool) ReSize(size int) {
	if size < p.Cap() {
		diff := p.Cap() - size
		for i := 0; i < diff; i++ {
			p.getWorker().stop()
		}
	} else if size == p.Cap() {
		return
	}
	atomic.StoreInt32(&p.capacity, int32(size))
}

协程池的包推荐:github.com/panjf2000/ants/v2

### 创建和管理 Golang 中的协程池Golang 中,通过使用 `goroutine` 可以轻松启动并发任务。然而,在处理大量并发请求时,无限制地创建新的 `goroutine` 会消耗过多内存并可能导致系统崩溃。因此,引入了协程池的概念来有效管理和重用已有的 `goroutine` 实例。 #### 使用第三方库 ants 构建协程池 为了简化开发过程并提升效率,推荐采用成熟的开源项目如蚂蚁金服团队贡献的 **ants** 库[^3]。该库提供了简单易用的功能接口用于构建高性能的应用场景下的协程池解决方案: - 安装方式如下所示: ```bash go get github.com/panjf2000/ants/v2 ``` - 初始化一个固定大小的工作线程池实例,并设置最大容量以及超时回收机制等参数配置: ```go import ( "fmt" "github.com/panjf2000/ants/v2" ) // 创建具有默认选项的新协程池 pool, err := ants.NewPool(10) if err != nil { fmt.Println(err) } defer pool.Release() ``` - 提交任务到工作队列等待被执行;这里需要注意的是提交的任务应当是一个实现了 `func()` 接口类型的匿名函数或者方法指针形式: ```go for i := 0; i < 5; i++ { err := pool.Submit(func() { fmt.Printf("running task %d\n", i) }) if err != nil { fmt.Printf("submitting task error: %s\n", err) } } ``` 上述代码片段展示了如何利用 `ants` 来初始化一个包含十个工作者的最大数量限制的协程池,并向其中注入五个简单的打印语句作为测试负载。 #### 手动实现简易版协程池 如果不想依赖外部包,则可以根据实际需求自定义一套基础版本的协程池逻辑框架。下面给出了一种可能的设计思路及其对应的伪代码表示法: - 预先分配一定数目的空闲通道缓冲区用来存储待命状态下的子进程; - 当有新作业到来时尝试从中取出可用者立即投入运行直至完成后再放回原处供后续调用; - 若当前所有成员均处于忙碌之中则暂时阻塞住直到某位成员释放出来为止继续往下走。 ```go type Task func() var workerCount int = 10 // 工作者数目 var queue chan Task // 待办事项列表 initWorkerPools() { // 启动工人们循环监听来自外界的消息通知 for i := 0; i < workerCount; i++ { go func(wid int) { for tsk := range queue { tsk() } }(i + 1) } } addTaskToQueue(task Task) bool{ select { case queue <- task: return true default: log.Fatal("queue is full") return false } } ``` 此段示例仅作概念性说明之用途并不具备完整的功能特性,具体细节还需要读者自行完善补充[^1]。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值