推荐一个Go语言的线程池开源框架 - ants「DaemonCoder」


9abdb80a168f9c2651b5c566a74312bb.png

1、为什么需要一个线程池

异步任务执行

线程池有很多应用场景,涉及异步线程去实现的逻辑往往都会用到线程池。比如我们用的web框架大多数在处理请求时,都会把接收到的请求交给线程池中的某个线程去执行,因为web接收请求的线程,不可能等待一个请求处理完再去接收下一个请求,都会把请求处理放到单独的线程池中去异步处理。

限制最大线程数

上面说的场景,对每个请求开一个新的线程就好了,为什么还要一个『池』呢?

想想如果有大量任务要执行,就会创建大量的线程,从而超出系统的负载,整体崩溃。所以稳定的系统是不能依赖任务数这种不可预估的因素的,需要对线程创建进行数量的管控,因此需要一个线程池来限制最大的线程数。

线程复用

创建线程是一个特别重的操作,之前创建过的线程如果执行完操作之后,再复用于后续的任务,这样就能大大减少线程创建的动作。

2、线程池的一种简单实现

package main


func main() {
        tasks := make(chan func())
        threadNum := 10
        for i := 0; i < threadNum; i++ {
                go Work(tasks)
        }
}


func Work(tasks chan func()) {
        for task := range tasks {
                // 忽略task执行异常处理
                task()
        }
}

上面的示例中,创建了10个线程组成的线程池,每个线程不断地从 tasks 中获取任务,然后执行。这简单几行代码似乎就是一个完美的线程池实现了,是的,大多数场景下这样实现完全足够。

缺点

使用线程池的缺点就是浪费内存,在没有任务执行时,上面的代码可以看到线程一直阻塞,并不会销毁线程。

3、Go开源线程池框架-ants

3.1、ants简介

ants是一个高性能的 goroutine 池,支持限制线程数量、线程复用。

Github地址:https://github.com/panjf2000/ants

核心特性:

  • 自动调度海量的 goroutines,复用 goroutines

  • 定期清理过期的 goroutines,进一步节省资源

  • 支持非阻塞提交任务

  • 预分配内存 (环形队列,可选)等

3.2、使用示例

package main


import (
        "sync"


        "github.com/panjf2000/ants/v2"
)


func main() {
        var wg sync.WaitGroup
        p, _ := ants.NewPool(10)
        defer p.Release()


        for i := 0; i < 100; i++ {
                wg.Add(1)
                p.Submit(func() {
                        // do something
                        wg.Done()
                })
        }
        wg.Wait()
}

4、ants的整体架构

4.1、处理流程图

f8a643cfa987f65139486b1275f97218.png

4.2、流程示例

如下图,4个线程,有6个任务提交的情况:

e1cfb453aa9bbf7fd7523afec1422655.png

其中4个任务从线程池中找到了空闲的线程来执行,剩余两个阻塞等待有线程释放会从线程池,如下图:

509af179d6ecefd7facc286367cc3cad.png

上一轮4个任务处理完成之后,剩下的两个任务开始被执行,此时有池程池中有两个空闲线程,如下图:

c4f8900055334dfe9601ca9693871b91.png

最后所有任务都执行完,线程池中有4个空闲线程,如下图:

d0b33585bb39a603bba824e5f00e6a90.png

5、ants关键源码分析

5.1、关键结构体

Pool结构:

type poolCommon struct {
        capacity int32
        workers workerQueue
        workerCache sync.Pool
        lock sync.Locker
        cond *sync.Cond
        // 其他字段忽略
}


type Pool struct {
        poolCommon
}
  • 其中workers是一个 worker数组,保存了线程运行体。

  • workerCache是创建worker的对应缓存,worker停止时不会真正的销毁,而是放入workerCache中。

  • lock用于实现操作线程池时加锁。

  • cond用于提交任务时线程池如果没有空闲worker时阻塞,等待线程池有空闲worker时再被唤醒。

  • 还有其他字段先暂忽略

Worker结构:

type goWorker struct {
        pool *Pool
        task chan func()
        lastUsed time.Time
}
  • pool字段和线程池相互引用

  • task是向worker提交任务的通道

  • lastUsed记录了worker上次处理任务的时间

5.2、线程池的创建

func NewPool(size int, options ...Option) (*Pool, error) {
        if size <= 0 {
                size = -1
        }


        opts := loadOptions(options...)


        if !opts.DisablePurge {
                if expiry := opts.ExpiryDuration; expiry < 0 {
                        return nil, ErrInvalidPoolExpiry
                } else if expiry == 0 {
                        opts.ExpiryDuration = DefaultCleanIntervalTime
                }
        }


        if opts.Logger == nil {
                opts.Logger = defaultLogger
        }


        p := &Pool{poolCommon: poolCommon{
                capacity: int32(size),
                allDone:  make(chan struct{}),
                lock:     syncx.NewSpinLock(),
                once:     &sync.Once{},
                options:  opts,
        }}
        p.workerCache.New = func() interface{} {
                return &goWorker{
                        pool: p,
                        task: make(chan func(), workerChanCap),
                }
        }
        if p.options.PreAlloc {
                if size == -1 {
                        return nil, ErrInvalidPreAllocSize
                }
                p.workers = newWorkerQueue(queueTypeLoopQueue, size)
        } else {
                p.workers = newWorkerQueue(queueTypeStack, 0)
        }


        p.cond = sync.NewCond(p.lock)


        p.goPurge()
        p.goTicktock()


        return p, nil
}

上面代码中可以看到 NewPool() 基本都是在给Pool结构体各个字段的初始化:

  • workerCache 的初始化方法中新创建一个goWorker结构,并和当前pool绑定。

  • workers:根据 PreAlloc 配置项是否开启情况,创建两种不同的worker数组实现方式:一种基于栈,另一种基于环形队列

初始化的最后,启动了两个异步线程:

  • goPurge:定期清理过期线程

  • goTicktock:定期更新当前时间缓存

5.3、任务提交流程

func (p *Pool) Submit(task func()) error {
        if p.IsClosed() {
                return ErrPoolClosed
        }


        w, err := p.retrieveWorker()
        if w != nil {
                w.inputFunc(task)
        }
        return err
}

任务提交时,通过retrieveWorker() 找到一个空闲worker,并调用inputFunc()把任务交给空闲worker处理。

retrieveWorker()是如何找到空闲worker的?

func (p *Pool) retrieveWorker() (w worker, err error) {
        p.lock.Lock()


retry:
        // 先从线程池中的workers中摘下来一个空闲worker,成功则直接返回
        if w = p.workers.detach(); w != nil {
                p.lock.Unlock()
                return
        }


        // 如果没有取到现有的空闲worker,则创建线程(其实是从workerCache中获取,他会自动管理从缓存中取还是真实创建)
        // 当然创建时会判断当前正在运行的worker是否达到了配置的最大限制
        if capacity := p.Cap(); capacity == -1 || capacity > p.Running() {
                p.lock.Unlock()
                w = p.workerCache.Get().(*goWorker)
                w.run()
                return
        }


        // 走到这里表示没有获取到可用worker,也不能创建新的(数量达到限制)
        // 根据是否配置了非阻塞模式,决定是直接返回错误,还是阻塞等待
        if p.options.Nonblocking || (p.options.MaxBlockingTasks != 0 && p.Waiting() >= p.options.MaxBlockingTasks) {
                p.lock.Unlock()
                // 非阻塞模式: 返回错误
                return nil, ErrPoolOverload
        }


        // 阻塞模式:在Pool.cond变量上阻塞,等待其他线程唤醒
        p.addWaiting(1)
        p.cond.Wait() // block and wait for an available worker
        p.addWaiting(-1)


        if p.IsClosed() {
                p.lock.Unlock()
                return nil, ErrPoolClosed
        }


        goto retry
}

上面代码可以看到,retrieveWorker() 方法中会不断循环尝试,直到获取到空闲worker。非阻塞模式下则会返回ErrPoolOverload的错误。

5.4、worker处理流程

func (w *goWorker) run() {
        w.pool.addRunning(1)
        go func() {
                defer func() {
                        if w.pool.addRunning(-1) == 0 && w.pool.IsClosed() {
                                w.pool.once.Do(func() {
                                        close(w.pool.allDone)
                                })
                        }
                        w.pool.workerCache.Put(w)
                        if p := recover(); p != nil {
                                if ph := w.pool.options.PanicHandler; ph != nil {
                                        ph(p)
                                } else {
                                        w.pool.options.Logger.Printf("worker exits from panic: %v\n%s\n", p, debug.Stack())
                                }
                        }
                        // Call Signal() here in case there are goroutines waiting for available workers.
                        w.pool.cond.Signal()
                }()


                for f := range w.task {
                        if f == nil {
                                return
                        }
                        f()
                        if ok := w.pool.revertWorker(w); !ok {
                                return
                        }
                }
        }()
}

上面代码可以看到,worker处理流程非常简单,就是不断从绑定的task通道中获取任务并调用,每处理完一个任务都会调用revertWorker()将当前worker再放加线程池的workers(空闲worker数组)中。

当worker生命周期结束时(线程数超限,或长时间没有新任务而被清理),调用w.pool.workerCache.Put(w)把当前的worker放到线程池的workerCache中,以便下次新创建线程时可以直接复用。

5.5、一些值得学习的细节

5.5.1、当前时间缓存

前面看在线程池创建的代码时,有一处goTicktock(),我们来看一下他的具体实现:

func (p *Pool) ticktock() {
        ticker := time.NewTicker(nowTimeUpdateInterval)
        defer func() {
                ticker.Stop()
                atomic.StoreInt32(&p.ticktockDone, 1)
        }()


        ticktockCtx := p.ticktockCtx // copy to the local variable to avoid race from Reboot()
        for {
                select {
                case <-ticktockCtx.Done():
                        return
                case <-ticker.C:
                }


                if p.IsClosed() {
                        break
                }


                p.now.Store(time.Now())
        }
}

默认每500ms获取一次系统时间,存储到线程池的now变量,worker每处理完一个任务,都会从这个变量中获取当前时间,并记录为worker的最后工作时间,后续清理任务用来判断是否需要清理此线程。

这样实现的意义在于,减少调用系统时间的次数,每500ms调用一次。如果不缓存当前时间的话,处理每个任务后都会调用一次系统时间,任务量较大时性能就会更差。

当然上面这种实现方式弊端就是当前时间不精确,只能精确到0.5秒级别,对于这个线程池这个项目来说,这个时间只用于线程清理,清理周期都是以秒为单位,时间精度的损失完全可以忽略不计。


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值