08-协程池

本文探讨了Go语言协程池的使用,分析了过多协程可能导致的问题,如内存开销、调度开销增加和系统资源消耗。提出了设计协程池的需求,包括任务队列、超时机制和异常处理,以及如何利用sync.Pool和sync.Cond进行协程管理和协调。
摘要由CSDN通过智能技术生成

协程池

go的优势是高并发,高并发是由go+channel的组合来完成的,那么这里,我们提出一个问题,go协程是否是创建的越多越好?

1. GMP模型

在这里插入图片描述

  • 每个 P 维护一个 G 的本地队列
  • 当一个 G 被创建出来,或者变为可执行状态时,优先把它放到 P 的本地队列中,否则放到全局队列
  • 当一个 G 在 M 里执行结束后,P 会从队列中把该 G 取出;如果此时 P 的队列为空,即没有其他 G 可以执行, M 会先尝试从全局队列寻找 G 来执行,如果全局队列为空,它会随机挑选另外一个 P,从它的队列里拿走一半 G 到自己的队列中执行

**P 的数量在默认情况下,会被设定为 CPU 的核数。而 M 虽然需要跟 P 绑定执行,但数量上并不与 P 相等。这是因为 M 会因为系统调用或者其他事情被阻塞,因此随着程序的执行,M 的数量可能增长,而 P 在没有用户干预的情况下,则会保持不变 **

1.1 大量创建go协程的代价

  • 内存开销

    go协程大约占2k的内存

    src/runtime/runtime2.go

    在这里插入图片描述

  • 调度开销

    虽然go协程的调度开销非常小,但也有一定的开销。

    runntime.Gosched()当前协程主动让出 CPU 去执行另外一个协程

  • gc开销

    协程占用的内存最终需要gc来回收

  • 隐性的CPU开销

    最终协程是要内核线程来执行,我们知道在GMP模型中,G阻塞后,会新创建M来执行,一个M往往对应一个内核线程,当创建大量go协程的时候,内核线程的开销可能也会增大

    GO: runtime: program exceeds 10000-thread limit
    

在这里插入图片描述

gmp模型中,本地队列的限制是256

  • 资源开销大的任务

    针对资源开销过大的任务,本身也不应当创建大量的协程,以免对CPU造成过大的压力,影响整体上的单机性能

  • 任务堆积

    当创建过多协程,G阻塞增多,本地队列堆积过多,很可能造成内存溢出

  • 系统任务影响

    runtime调度,gc等都是运行在go协程上的,当goroutine规模过大,会影响其他任务

2. 协程池

基于以上一些理由,有必要创建一个协程池,将协程有效的管理起来,不要随意的创建过多的协程。

同时池化的核心在于复用,所以我们可以这么想,一个goroutine是否可以处理多个任务,而不是一个goroutine处理一个任务

2.1 需求

我们先来罗列一下我们的需求:

  1. 希望创建固定数量的协程
  2. 有一个任务队列,等待协程进行调度执行
  3. 协程用完时,其他任务处于等待状态,一旦有协程空余,立即获取任务执行
  4. 协程长时间空余,清理,以免占用空间
  5. 有超时时间,如果一个任务长时间完成不了,就超时,让出协程

2.2 设计

在这里插入图片描述

2.3 初步实现

package mspool

import (
	"errors"
	"sync"
	"sync/atomic"
	"time"
)

type sig struct{}

const DefaultExpire = 3

var (
	ErrorInValidCap    = errors.New("pool cap can not <= 0")
	ErrorInValidExpire = errors.New("pool expire can not <= 0")
	ErrorHasClosed     = errors.New("pool has bean released!!")
)

type Pool struct {
	//cap 容量 pool max cap
	cap int32
	//running 正在运行的worker的数量
	running int32
	//空闲worker
	workers []*Worker
	//expire 过期时间 空闲的worker超过这个时间 回收掉
	expire time.Duration
	//release 释放资源  pool就不能使用了
	release chan sig
	//lock 去保护pool里面的相关资源的安全
	lock sync.Mutex
	//once 释放只能调用一次 不能多次调用
	once sync.Once
}

func NewPool(cap int) (*Pool, error) {
	return NewTimePool(cap, DefaultExpire)
}

func NewTimePool(cap int, expire int) (*Pool, error) {
	if cap <= 0 {
		return nil, ErrorInValidCap
	}
	if expire <= 0 {
		return nil, ErrorInValidExpire
	}
	p := &Pool{
		cap:     int32(cap),
		expire:  time.Duration(expire) * time.Second,
		release: make(chan sig, 1),
	}
	go expireWorker()
	return p, nil
}

func expireWorker() {
	//定时清理过期的空闲worker

}

//提交任务

func (p *Pool) Submit(task func()) error {
	if len(p.release) > 0 {
		return ErrorHasClosed
	}
	//获取池里面的一个worker,然后执行任务就可以了
	w := p.GetWorker()
	w.task <- task
	w.pool.incRunning()
	return nil
}

func (p *Pool) GetWorker() *Worker {
	//1. 目的获取pool里面的worker
	//2. 如果 有空闲的worker 直接获取

	idleWorkers := p.workers
	n := len(idleWorkers) - 1
	if n >= 0 {
		p.lock.Lock()
		w := idleWorkers[n]
		idleWorkers[n] = nil
		p.workers = idleWorkers[:n]
		p.lock.Unlock()
		return w
	}
	//3. 如果没有空闲的worker,要新建一个worker
	if p.running < p.cap {
		//还不够pool的容量,直接新建一个
		w := &Worker{
			pool: p,
			task: make(chan func(), 1),
		}
		w.run()
		return w
	}
	//4. 如果正在运行的workers 如果大于pool容量,阻塞等待,worker释放
	for {
		p.lock.Lock()
		idleWorkers := p.workers
		n := len(idleWorkers) - 1
		if n < 0 {
			p.lock.Unlock()
			continue
		}
		w := idleWorkers[n]
		idleWorkers[n] = nil
		p.workers = idleWorkers[:n]
		p.lock.Unlock()
		return w
	}
}

func (p *Pool) incRunning() {
	atomic.AddInt32(&p.running, 1)
}

func (p *Pool) PutWorker(w *Worker) {
	w.lastTime = time.Now()
	p.lock.Lock()
	p.workers = append(p.workers, w)
	p.lock.Unlock()
}

func (p *Pool) decRunning() {
	atomic.AddInt32(&p.running, -1)
}

func (p *Pool) Release() {
	p.once.Do(func() {
		//只执行一次
		p.lock.Lock()
		workers := p.workers
		for i, w := range workers {
			w.task = nil
			w.pool = nil
			workers[i] = nil
		}
		p.workers = nil
		p.lock.Unlock()
		p.release <- sig{}
	})
}

func (p *Pool) IsClosed() bool {

	return len(p.release) > 0
}

func (p *Pool) Restart() bool {
	if len(p.release) <= 0 {
		return true
	}
	_ = <-p.release
	return true
}

package mspool

import (
	"time"
)

type Worker struct {
	pool *Pool
	//task 任务队列
	task chan func()
	//lastTime 执行任务的最后的时间
	lastTime time.Time
}

func (w *Worker) run() {
	go w.running()
}

func (w *Worker) running() {
	for f := range w.task {
		if f == nil {
			return
		}
		f()
		//任务运行完成,worker空闲
		w.pool.PutWorker(w)
		w.pool.decRunning()
	}
}

2.4 定时清除无用的worker


func (p *Pool) expireWorker() {
	ticker := time.NewTicker(p.expire)
	for range ticker.C {
		currentTime := time.Now()
		if p.IsRelease() {
			break
		}
		p.lock.Lock()
		idleWorkers := p.workers
		n := -1
		for i, w := range idleWorkers {
			if currentTime.Sub(w.lastTime) <= p.expire {
				break
			}
			//需要清除的
			n = i
			w.task <- nil
			idleWorkers[i] = nil
		}
		if n > -1 {
			if n >= len(idleWorkers)-1 {
				p.workers = idleWorkers[:0]
			} else {
				p.workers = idleWorkers[n+1:]
			}
		}
		p.lock.Unlock()
	}
}

2.5 引入sync.pool

在前面我们已经用过pool了,这里我们可以将worker的创建也放入pool中提前暴露(缓存),用的时候从pool中获取,用完在还回pool中,这样性能更高


func (p *Pool) GetWorker() *Worker {
	//1. 目的获取pool里面的worker
	//2. 如果 有空闲的worker 直接获取

	idleWorkers := p.workers
	n := len(idleWorkers) - 1
	if n >= 0 {
		p.lock.Lock()
		w := idleWorkers[n]
		idleWorkers[n] = nil
		p.workers = idleWorkers[:n]
		p.lock.Unlock()
		return w
	}
	//3. 如果没有空闲的worker,要新建一个worker
	if p.running < p.cap {
		//还不够pool的容量,直接新建一个
		c := p.workerCache.Get()
		var w *Worker
		if c == nil {
			w = &Worker{
				pool: p,
				task: make(chan func(), 1),
			}
		} else {
			w = c.(*Worker)
		}

		w.run()
		return w
	}
	//4. 如果正在运行的workers 如果大于pool容量,阻塞等待,worker释放
	for {
		p.lock.Lock()
		idleWorkers := p.workers
		n := len(idleWorkers) - 1
		if n < 0 {
			p.lock.Unlock()
			continue
		}
		w := idleWorkers[n]
		idleWorkers[n] = nil
		p.workers = idleWorkers[:n]
		p.lock.Unlock()
		return w
	}
}
package mspool

import (
	"time"
)

type Worker struct {
	pool *Pool
	//task 任务队列
	task chan func()
	//lastTime 执行任务的最后的时间
	lastTime time.Time
}

func (w *Worker) run() {
	go w.running()
}

func (w *Worker) running() {
	for f := range w.task {
		if f == nil {
			w.pool.workerCache.Put(w)
			return
		}
		f()
		//任务运行完成,worker空闲
		w.pool.PutWorker(w)
		w.pool.decRunning()
	}
}

2.6 引入sync.Cond

sync.Cond 是基于互斥锁/读写锁实现的条件变量,用来协调想要访问共享资源的那些 Goroutine。

当共享资源状态发生变化时,sync.Cond 可以用来通知等待条件发生而阻塞的 Goroutine。

在上述的场景中,我们可以将其应用在等待worker那里,可以使用sync.Cond阻塞,当worker执行完任务后,通知其继续执行。

  • Signal方法:允许调用者Caller唤醒一个等待此Cond和goroutine。如果此时没有等待的goroutine,显然无需通知waiter;如果Cond等待队列中有一个或者多个等待的goroutine,则需要从等待队列中移除第一个goroutine并把它唤醒。在Java语言中,Signal方法也叫做notify方法。调用Signal方法时,不强求你一定要持有c.L的锁。
  • Broadcast方法,允许调用者Caller唤醒所有等待此Cond的goroutine。如果此时没有等待的goroutine,显然无需通知waiter;如果Cond等待队列中有一个或者多个等待的goroutine,则清空所有等待的goroutine,并全部唤醒。在Java语言中,Broadcast方法也被叫做notifyAll方法。同样地,调用Broadcast方法时,也不强求你一定持有c.L的锁。
  • Wait方法,会把调用者Caller放入Cond的等待队列中并阻塞,直到被Signal或者Broadcast的方法从等待队列中移除并唤醒。调用Wait方法时必须要持有c.L的锁。

func (p *Pool) waitIdleWorker() *Worker {
	p.lock.Lock()
	p.cond.Wait()
	fmt.Println("被唤醒")
	idleWorkers := p.workers
	n := len(idleWorkers) - 1
	if n < 0 {
		p.lock.Unlock()
		return p.waitIdleWorker()
	}
	w := idleWorkers[n]
	idleWorkers[n] = nil
	p.workers = idleWorkers[:n]
	p.lock.Unlock()
	return w
}
func NewTimePool(cap int, expire int) (*Pool, error) {
	if cap <= 0 {
		return nil, ErrInvalidCap
	}
	if expire <= 0 {
		return nil, ErrInvalidCap
	}
	p := &Pool{
		cap:     int32(cap),
		expire:  time.Duration(expire) * time.Second,
		release: make(chan sig, 1),
	}
	p.workerCache.New = func() any {
		return &Worker{
			pool: p,
			task: make(chan func(), 1),
		}
	}
	p.cond = sync.NewCond(&p.lock)
	go p.expireWorker()
	return p, nil
}
func (p *Pool) PutWorker(w *Worker) {
	w.lastTime = time.Now()
	p.lock.Lock()
	p.workers = append(p.workers, w)
	p.cond.Signal()
	p.lock.Unlock()
}

2.7 任务超时释放

针对任务超时,需要使用工具的开发者,在程序中自动处理,及时退出goroutine

2.8 异常处理

当task发生问题时,需要能捕获到,对外提供入口,让开发者自定义错误处理方式

package mspool

import (
	msLog "github.com/mszlu521/msgo/log"
	"time"
)

type Worker struct {
	pool *Pool
	//task 任务队列
	task chan func()
	//lastTime 执行任务的最后的时间
	lastTime time.Time
}

func (w *Worker) run() {
	go w.running()
}

func (w *Worker) running() {
	defer func() {
		w.pool.decRunning()
		w.pool.workerCache.Put(w)
		if err := recover(); err != nil {
			//捕获任务发生的panic
			if w.pool.PanicHandler != nil {
				w.pool.PanicHandler()
			} else {
				msLog.Default().Error(err)
			}
		}
		w.pool.cond.Signal()
	}()
	for f := range w.task {
		if f == nil {
			w.pool.workerCache.Put(w)
			return
		}
		f()
		//任务运行完成,worker空闲
		w.pool.PutWorker(w)
		w.pool.decRunning()
	}
}

//PanicHandler
	PanicHandler func(any)

func (p *Pool) waitIdleWorker() *Worker {
	p.lock.Lock()
	p.cond.Wait()

	idleWorkers := p.workers
	n := len(idleWorkers) - 1
	if n < 0 {
		p.lock.Unlock()
		if p.running < p.cap {
			//还不够pool的容量,直接新建一个
			c := p.workerCache.Get()
			var w *Worker
			if c == nil {
				w = &Worker{
					pool: p,
					task: make(chan func(), 1),
				}
			} else {
				w = c.(*Worker)
			}
			w.run()
			return w
		}
		return p.waitIdleWorker()
	}
	w := idleWorkers[n]
	idleWorkers[n] = nil
	p.workers = idleWorkers[:n]
	p.lock.Unlock()
	return w
}

2.9 性能测试

package mspool

import (
	"math"
	"runtime"
	"sync"
	"testing"
	"time"
)

const (
	_   = 1 << (10 * iota)
	KiB // 1024
	MiB // 1048576
	// GiB // 1073741824
	// TiB // 1099511627776             (超过了int32的范围)
	// PiB // 1125899906842624
	// EiB // 1152921504606846976
	// ZiB // 1180591620717411303424    (超过了int64的范围)
	// YiB // 1208925819614629174706176
)

const (
	Param    = 100
	PoolSize = 1000
	TestSize = 10000
	n        = 1000000
)

var curMem uint64

const (
	RunTimes           = 1000000
	BenchParam         = 10
	DefaultExpiredTime = 10 * time.Second
)

func demoFunc() {
	time.Sleep(time.Duration(BenchParam) * time.Millisecond)
}

func TestNoPool(t *testing.T) {
	var wg sync.WaitGroup
	for i := 0; i < n; i++ {
		wg.Add(1)
		go func() {
			demoFunc()
			wg.Done()
		}()
	}

	wg.Wait()
	mem := runtime.MemStats{}
	runtime.ReadMemStats(&mem)
	curMem = mem.TotalAlloc/MiB - curMem
	t.Logf("memory usage:%d MB", curMem)
}

func TestHasPool(t *testing.T) {
	pool, _ := NewPool(math.MaxInt32)
	defer pool.Release()
	var wg sync.WaitGroup
	for i := 0; i < n; i++ {
		wg.Add(1)
		_ = pool.Submit(func() {
			demoFunc()
			wg.Done()
		})
	}
	wg.Wait()

	mem := runtime.MemStats{}
	runtime.ReadMemStats(&mem)
	curMem = mem.TotalAlloc/MiB - curMem
	t.Logf("memory usage:%d MB", curMem)
}

bug修改:


func (p *Pool) expireWorker() {
	//定时清理过期的空闲worker
	ticker := time.NewTicker(p.expire)
	for range ticker.C {
		if p.IsClosed() {
			break
		}
		//循环空闲的workers 如果当前时间和worker的最后运行任务的时间 差值大于expire 进行清理
		p.lock.Lock()
		idleWorkers := p.workers
		n := len(idleWorkers) - 1
		if n >= 0 {
			var clearN = -1
			for i, w := range idleWorkers {
				if time.Now().Sub(w.lastTime) <= p.expire {
					break
				}
				clearN = i
				w.task <- nil
				idleWorkers[i] = nil
			}
			// 3 2
			if clearN != -1 {
				if clearN >= len(idleWorkers)-1 {
					p.workers = idleWorkers[:0]
				} else {
					// len=3 0,1 del 2
					p.workers = idleWorkers[clearN+1:]
				}
				fmt.Printf("清除完成,running:%d, workers:%v \n", p.running, p.workers)
			}
		}
		p.lock.Unlock()
	}
}

er.C {
if p.IsClosed() {
break
}
//循环空闲的workers 如果当前时间和worker的最后运行任务的时间 差值大于expire 进行清理
p.lock.Lock()
idleWorkers := p.workers
n := len(idleWorkers) - 1
if n >= 0 {
var clearN = -1
for i, w := range idleWorkers {
if time.Now().Sub(w.lastTime) <= p.expire {
break
}
clearN = i
w.task <- nil
idleWorkers[i] = nil
}
// 3 2
if clearN != -1 {
if clearN >= len(idleWorkers)-1 {
p.workers = idleWorkers[:0]
} else {
// len=3 0,1 del 2
p.workers = idleWorkers[clearN+1:]
}
fmt.Printf(“清除完成,running:%d, workers:%v \n”, p.running, p.workers)
}
}
p.lock.Unlock()
}
}


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值