Go 并发就是如此简单——带你走读 Ants 🐜🐜🐜

719 阅读17分钟

ants 是一个高性能的 goroutine 池,实现了对大规模 goroutine 的调度管理、goroutine 复用,允许使用者在开发并发程序的时候限制 goroutine 数量,复用资源,达到更高效执行任务的效果。

功能

  • 自动调度海量的 goroutines,复用 goroutines
  • 定期清理过期的 goroutines,进一步节省资源
  • 提供了大量实用的接口:任务提交、获取运行中的 goroutine 数量、动态调整 Pool 大小、释放 Pool、重启 Pool 等
  • 优雅处理 panic,防止程序崩溃
  • 资源复用,极大节省内存使用量;在大规模批量并发任务场景下甚至可能比 Go 语言的无限制 goroutine 并发具有更高的性能
  • 非阻塞机制
  • 预分配内存 (环形队列,可选)

工作流程

flowchart TD
    A[初始化 goroutine 池] -->|提交一个任务| B[工作池]
    
    C{池中是否有可用的 worker?} -->|是| G[取出 worker 来执行任务]
    C -->|否| D{工作池容量是否用完}
    
    B --> C
    
    D -->|是| E{工作池是否为非阻塞模式?}
    D -->|否| F[新启动一个 worker 来执行任务]
    
    E -->|是| H[直接返回 nil]
    E -->|否| I[阻塞等待可用的 worker]
    
    I --> G
    
    F --> B
    G --> |完成任务后将 worker 放回工作池| B

快速开始

安装

go get -u github.com/panjf2000/ants/v2

使用

  • Default Pool
package main
​
import (
    "fmt"
    "sync"
    
    "github.com/panjf2000/ants/v2"
)
​
func main() {
    var wg sync.WaitGroup
    
    // Submit tasks to the default pool
    for i := 0; i < 10; i++ {
        wg.Add(1)
        ants.Submit(func() {
            // Your task here
            fmt.Println("Task is running")
            wg.Done()
        })
    }
    
    wg.Wait()
    fmt.Printf("Running goroutines: %d\n", ants.Running())
    
    // Release the default pool when finished
    ants.Release()
}
  • Basic Pool
package main
​
import (
    "fmt"
    "sync"
    
    "github.com/panjf2000/ants/v2"
)
​
func main() {
    var wg sync.WaitGroup
    
    // Create a pool with capacity of 10 workers
    pool, _ := ants.NewPool(10)
    defer pool.Release()
    
    // Submit tasks
    for i := 0; i < 20; i++ {
        wg.Add(1)
        pool.Submit(func() {
            fmt.Println("Task is running")
            wg.Done()
        })
    }
    
    wg.Wait()
    fmt.Printf("Running goroutines: %d\n", pool.Running())
}
  • Pool with Function
package main
​
import (
    "fmt"
    "sync"
    
    "github.com/panjf2000/ants/v2"
)
​
func main() {
    var wg sync.WaitGroup
    
    // Create a pool with capacity of 10 workers
    // that runs the same function for all tasks
    pool, _ := ants.NewPoolWithFunc(10, func(i interface{}) {
        n := i.(int)
        fmt.Printf("Processing task: %d\n", n)
        wg.Done()
    })
    defer pool.Release()
    
    // Submit tasks
    for i := 1; i <= 20; i++ {
        wg.Add(1)
        pool.Invoke(i) // Pass the argument to the function
    }
    
    wg.Wait()
    fmt.Printf("Running goroutines: %d\n", pool.Running())
}
  • Type-Safe Generic Pool
package main
​
import (
    "fmt"
    "sync"
    
    "github.com/panjf2000/ants/v2"
)
​
func main() {
    var wg sync.WaitGroup
    
    // Create a generic pool with capacity of 10 workers
    // that runs the same function for all tasks of type int
    pool, _ := ants.NewPoolWithFuncGeneric(10, func(n int) {
        fmt.Printf("Processing task: %d\n", n)
        wg.Done()
    })
    defer pool.Release()
    
    // Submit tasks
    for i := 1; i <= 20; i++ {
        wg.Add(1)
        pool.Invoke(i) // Type-safe argument
    }
    
    wg.Wait()
    fmt.Printf("Running goroutines: %d\n", pool.Running())
}

核心概念

池化

Go 的 goroutine 轻量级,但创建和销毁大量 goroutine 仍然可能影响性能和内存使用。goroutine 池通过以下方式解决这个问题:

  • 管理和限制活动 goroutine 的数量
  • 通过回收 goroutine 减少内存分配开销
  • 提供受控的并发机制
  • 提升高吞吐量场景下的性能
embeds
contains multiple
poolCommon
+int32 capacity
+int32 running
+sync.Locker lock
+workerQueue workers
+int32 state
+*sync.Cond cond
+chan struct allDone
+*sync.Once once
+sync.Pool workerCache
+int32 waiting
+int32 purgeDone
+context.Context purgeCtx
+context.CancelFunc stopPurge
+atomic.Value now
+*Options options
+purgeStaleWorkers()
+ticktock()
+goPurge()
+goTicktock()
+nowTime() : time.Time
+Running() : int
+Free() : int
+Waiting() : int
+Cap() : int
+Tune(size int)
+IsClosed()
+Release()
+ReleaseTimeout(timeout time.Duration) : error
+Reboot()
+addRunning(delta int) : int
+addWaiting(delta int)
+retrieveWorker()(worker, error)
+revertWorker(worker) : bool
Pool
+Submit(task func()) : error
+Submit(task func()) : error
+Running() : int
+Free() : int
+Waiting() : int
+Cap() : int
+Tune(size int)
+ReleaseTimeout(timeout time.Duration) : error
+Reboot()
MultiPool
- []*Pool pools
- uint32 index
- int32 state
- LoadBalancingStrategy lbs
+Submit(task func()) : error
+Running() : int
+RunningByIndex(idx int)(int, error)
+Free() : int
+FreeByIndex(idx int)(int, error)
+Waiting() : int
+WaitingByIndex(idx int)(int, error)
+Cap() : int
+Tune(size int)
+IsClosed() : bool
+ReleaseTimeout(timeout time.Duration) : error
+Reboot()

Pool

ants 中,提供了 Basic Pool 与 Multi Pool 两种实现

  • Basic Pool

    保持对 goroutine 最大数量的控制的同时支持并发调用任意函数,主要通过 goroutine 的复用来减少对 goroutine 创建与销毁时的开销

    • Running() int 返回 Running 状态的 goroutine 的数量
    • Free() int 返回可用的 goroutine 数量
    • Cap() int 返回 Pool 的容量
    • Tune(size int) 动态扩缩 Pool 的容量
    • IsClosed() bool Pool 是否关闭
    • Release() 关闭并释放资源
    • ReleaseTimeout(timeout time.Duration) error 等待后释放
    • Reboot() 重启 Pool
Submit tasks
Submit more tasks
Release()
Resources freed
Reboot()
Submit tasks
NewPool
Created
Running
Released
Rebooted
  • Multi Pool

    • Running() int 返回所有 Pool 中 Running 状态的 goroutine 的数量
    • Free() int 返回所有 Pool 中可用的 goroutine 数量
    • FreeByIndex(idx int) (int, error) 返回指定 Pool 中 Free 状态的 goroutine 的数量
    • Waiting() (int, error) 返回所有 Pool 中等待任务的总数
    • WaitingByIndex(idx int) (int, error) 返回指定 Pool 中等待任务的数量
    • Cap() int 返回所有 Pool 的容量
    • Tune(size int) 动态扩缩 Pool 的容量
    • IsClosed() Pool 是否关闭
    • ReleaseTimeout(timeout time.Duration) error 延迟一定时间关闭
    • Reboot() 重启 Pool
if strategy == RoundRobin
if strategy == LeastTasks
if pool accepts task
if ErrPoolOverload
if already using LeastTasks
if RoundRobin was used initially
SubmitTask
CheckStrategy
RoundRobin
Increment counter and get next pool
SelectNextPool
SubmitToPool
LeastTasks
Scan all pools
FindPoolWithFewestTasks
TrySubmit
Success
Failure
★ Success End
ReturnError
★ Failure End
FallbackStrategy
Use LeastTasks as fallback

状态流转

worker.run()
Pool closed
Task completed
Purged after ExpiryDuration
Pool closed
NewPool
Created
Running
Processing
Receive
Idle

源码解读

创建流程

pool.workerCache.NewPoolnewPoolants.NewPoolCallerpool.workerCache.NewPoolnewPoolants.NewPoolCalleralt[if err == nil]NewPool(size, options...)newPool(size, options...)poolCommon, err创建 Pool 实例设置 workerCache.New返回 goWorker 实例创建函数*Pool, error
// NewPool instantiates a Pool with customized options.
func NewPool(size int, options ...Option) (*Pool, error) {
	pc, err := newPool(size, options...)
	if err != nil {
		return nil, err
	}

	pool := &Pool{poolCommon: pc}
	pool.workerCache.New = func() any {
		return &goWorker{
			pool: pool,
			task: make(chan func(), workerChanCap),
		}
	}

	return pool, nil
}
  • 创建 poolCommon

    func newPool(size int, options ...Option) (*poolCommon, 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 := &poolCommon{
    		capacity: int32(size),
    		allDone:  make(chan struct{}),
    		lock:     syncx.NewSpinLock(),
    		once:     &sync.Once{},
    		options:  opts,
    	}
    	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
    }
    

    在当前部分的代码中主要完成了对于公共参数的初始化、清理流程与定时流程创建

  • 创建 任务队列

    	pool.workerCache.New = func() any {
    		return &goWorker{
    			pool: pool,
    			task: make(chan func(), workerChanCap),
    		}
    	}
    

任务提交

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

	w, err := p.retrieveWorker()
	if w != nil {
		w.inputFunc(task)
	}
	return err
}
  • 判断当前 Pool 是否被关闭

  • 获取 goWorker

  • 将任务放入任务队列 w.inputFunc(task)

    func (w *goWorker) inputFunc(fn func()) {
    	w.task <- fn
    }
    

任务执行

// run starts a goroutine to repeat the process
// that performs the function calls.
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 fn := range w.task {
			if fn == nil {
				return
			}
			fn()
			if ok := w.pool.revertWorker(w); !ok {
				return
			}
		}
	}()
}
  • 启动工作协程

    w.pool.addRunning(1) 增加了当前运行的工作协程计数,表示有一个新的工作协程启动。随后,通过 go func() 启动一个新的协程来处理任务。

    w.pool.addRunning(1)
    go func() {
        defer func() { ... }()
        for fn := range w.task { ... }
    }()
    
  • 清理工作协程的状态与错误捕获

    • 清理工作状态

      减少运行中的工作协程计数,并检查如果池已关闭且没有运行的协程,则触发 allDone 通道的关闭信号。接着,将当前的 goWorker 对象放回 workerCache 缓存池中以供复用。

      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)
          ...
      }()
      
    • 捕获异常状态

      如果发生异常,会调用用户定义的 PanicHandler,或者记录日志以便调试。

      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())
      	}
      }
      
    • 状态同步

      借助 sync.Cond 同步当前状态

      	w.pool.cond.Signal()
      
  • 执行任务并归还当前协程

    for fn := range w.task {
    	if fn == nil {
    		return
    	}
    	fn()
    	if ok := w.pool.revertWorker(w); !ok {
    		return
    	}
    }
    
    • 任务执行 fn()
    • 归还协程 revertWorker(w *goWorker) bool

负载均衡

func (mp *MultiPool) next(lbs LoadBalancingStrategy) (idx int) {
	switch lbs {
	case RoundRobin:
		return int(atomic.AddUint32(&mp.index, 1) % uint32(len(mp.pools)))
	case LeastTasks:
		leastTasks := 1<<31 - 1
		for i, pool := range mp.pools {
			if n := pool.Running(); n < leastTasks {
				leastTasks = n
				idx = i
			}
		}
		return
	}
	return -1
}
  • RoundRobin

    • 将任务依次分配到每个池中,适合任务负载较为均匀的场景
    • 通过原子操作递增 mp.index,并对池的数量取模,确保索引在有效范围内循环
  • LeastTasks

    • 适合任务负载不均匀的场景,可以动态选择最空闲的池
    • 遍历所有池,比较每个池中当前运行的任务数 pool.Running(),选择任务数最少的池的索引
if strategy == RoundRobin
if strategy == LeastTasks
if ErrPoolOverload
if pool accepts task
if RoundRobin was used
initially
if already using LeastTasks
retry
●
SubmitTask
CheckStrategy
RoundRobin
LeastTasks
Increment counter and get
next pool
SelectNextPool
SubmitToPool
Scan all pools
FindPoolWithFewestTasks
TrySubmit
Failure
Success
FallbackStrategy
ReturnError
Use LeastTasks as fallback
●

工作队列

implements
implements
«interface»
Worker
-len() : int
-isEmpty() : bool
-insert(worker) : error
-detach() : worker
-refresh(time.Duration) : []worker
-reset()
workerStack
-items []worker
-expiry []worker
workerLoopQueue
-items []worker
-expiry []worker
-head int
-tail int
-size int
-isFull bool
  • len() 返回队列中元素的数量
  • isEmpty() 检查队列是否为空
  • insert(worker) 向队列中添加一个元素
  • detach() 从队列中移除元素
  • refresh(time.Duration) 根据过期时间移除过期元素
  • reset() 重置队列

构造函数

提供了一个工厂函数来创建适当的队列类型:

func newWorkerQueue(qType queueType, size int) workerQueue

队列类型

ants 定义了两种队列类型作为常量:

const (
    queueTypeStack queueType = 1 << iota
    queueTypeLoopQueue
)
  • 工作栈实现

    工作栈是基于动态切片实现的先进后出(LIFO)队列。

    type workerStackstruct {
        items  []worker // Slice to store workers
        expiry []worker // Temporary storage for expired workers
    }
    
    • 插入操作

      向栈末尾添加一个工作线程,具有 O(1) 平摊复杂度:

      func (ws *workerStack) insert(w worker)error {
        ws.items = append(ws.items, w)
      	return nil
      }
      

      由于工作栈没有大小限制,此操作永远不会失败。

    • 弹出操作

      移除并返回最近添加的工作者,复杂度为 O(1):

      func (ws *workerStack) detach() worker {
        l := ws.len()
      	if l == 0 {
      		return nil
        }
      
        w := ws.items[l-1]
        ws.items[l-1] = nil // avoid memory leaks
        ws.items = ws.items[:l-1]
      
      	return w
      }
      
    • 刷新操作

      使用二分搜索来识别和移除超过指定持续时间的空闲工蚁:

      func (ws *workerStack) refresh(duration time.Duration) []worker {
      	n := ws.len()
      	if n == 0 {
      		return nil
      	}
      
      	expiryTime := time.Now().Add(-duration)
      	index := ws.binarySearch(0, n-1, expiryTime)
      
      	ws.expiry = ws.expiry[:0]
      	if index != -1 {
      		ws.expiry = append(ws.expiry, ws.items[:index+1]...)
      		m := copy(ws.items, ws.items[index+1:])
      		for i := m; i < n; i++ {
      			ws.items[i] = nil
      		}
      		ws.items = ws.items[:m]
      	}
      	return ws.expiry
      }
      
      func (ws *workerStack) binarySearch(l, r int, expiryTime time.Time) int {
      	for l <= r {
      		mid := l + ((r - l) >> 1) // avoid overflow when computing mid
      		if expiryTime.Before(ws.items[mid].lastUsedTime()) {
      			r = mid - 1
      		} else {
      			l = mid + 1
      		}
      	}
      	return r
      }
      

      二分搜索有效地找到陈旧和新鲜工作者的边界,具有 O(\log n) 的复杂度。

  • 工作循环队列实现

    工作循环队列是基于环形固定大小数组的先进先出(FIFO)队列。

    type loopQueuestruct {
        items  []worker // Fixed-size array for workers
        expiry []worker // Temporary storage for expired workers
        headint // Index for the first element
        tailint // Index for the next insertion
        sizeint // Fixed size of the queue
        isFullbool // Flag indicating if the queue is full
    }
    
    • 插入操作

      添加一个工作节点到尾部,具有 O(1) 复杂度:

      func (wq *loopQueue)insert(w worker)error {
      	if wq.isFull {
      		return errQueueIsFull
        }
        wq.items[wq.tail] = w
        wq.tail = (wq.tail + 1) % wq.size
      
      	if wq.tail == wq.head {
      		wq.isFull = true
        }
      
      	return nil
      }
      

      由于循环队列具有固定大小,如果队列已满,此操作可能会失败并显示 errQueueIsFull or 陷入等待。

    • 删除操作

      移除并返回头位置的最早工作进程,具有 O(1) 复杂度:

      func (wq *loopQueue) detach() worker {
      	if wq.isEmpty() {
      		return nil
        }
      
        w := wq.items[wq.head]
        wq.items[wq.head] = nil
        wq.head = (wq.head + 1) % wq.size
      
        wq.isFull = false
      
      	return w
      }
      

      实现了先进先出(FIFO)行为,为工作者提供公平的调度。

    • 刷新操作

      使用二分搜索算法来识别和删除循环数组中的过时工作节点:

      func (wq *loopQueue) refresh(duration time.Duration) []worker {
        expiryTime := time.Now().Add(-duration)
        index := wq.binarySearch(expiryTime)
      	// Remove and return expired workers from the circular array
      	if index == -1 {
      		return nil
      	}
      	wq.expiry = wq.expiry[:0]
      
      	if wq.head <= index {
      		wq.expiry = append(wq.expiry, wq.items[wq.head:index+1]...)
      		for i := wq.head; i < index+1; i++ {
      			wq.items[i] = nil
      		}
      	} else {
      		wq.expiry = append(wq.expiry, wq.items[0:index+1]...)
      		wq.expiry = append(wq.expiry, wq.items[wq.head:]...)
      		for i := 0; i < index+1; i++ {
      			wq.items[i] = nil
      		}
      		for i := wq.head; i < wq.size; i++ {
      			wq.items[i] = nil
      		}
      	}
      	head := (index + 1) % wq.size
      	wq.head = head
      	if len(wq.expiry) > 0 {
      		wq.isFull = false
      	}
      
      	return wq.expiry
      }
      

Focus Case

过期机制

两个队列实现都提供了一个 refresh 方法,用于在一段时间后移除空闲的工作者。这对于内存管理至关重要,尤其是在长时间运行的应用程序中。

刷新机制特别有趣,因为它使用二分搜索高效地找到过期边界,工作进程按最后使用时间排序。我们可以看一下其在 worker loop queue 中的实现

func (wq *loopQueue) binarySearch(expiryTime time.Time) int {
	var mid, nlen, basel, tmid int
	nlen = len(wq.items)

	// if no need to remove work, return -1
	if wq.isEmpty() || expiryTime.Before(wq.items[wq.head].lastUsedTime()) {
		return -1
	}

	// example
	// size = 8, head = 7, tail = 4
	// [ 2, 3, 4, 5, nil, nil, nil,  1]  true position
	//   0  1  2  3    4   5     6   7
	//              tail          head
	//
	//   1  2  3  4  nil nil   nil   0   mapped position
	//            r                  l

	// base algorithm is a copy from worker_stack
	// map head and tail to effective left and right
	r := (wq.tail - 1 - wq.head + nlen) % nlen
	basel = wq.head
	l := 0
	for l <= r {
		mid = l + ((r - l) >> 1) // avoid overflow when computing mid
		// calculate true mid position from mapped mid position
		tmid = (mid + basel + nlen) % nlen
		if expiryTime.Before(wq.items[tmid].lastUsedTime()) {
			r = mid - 1
		} else {
			l = mid + 1
		}
	}
	// return true position from mapped position
	return (r + basel + nlen) % nlen
}
  1. 首先进行边界检查

    if wq.isEmpty() || expiryTime.Before(wq.items[wq.head].lastUsedTime()) {
        return -1
    }
    
  2. 将循环队列的逻辑索引映射为线性索引

    r := (wq.tail - 1 - wq.head + nlen) % nlen
    basel = wq.head
    l := 0
    

    将循环队列的逻辑索引映射为线性索引,以便进行二分查找。通过计算尾部和头部的相对位置,确定有效的查找范围,并将队列头部作为基准索引。这一点的设计真的非常有意思🤔

  3. 计算中间值

    mid = l + ((r - l) >> 1)
    tmid = (mid + basel + nlen) % nlen
    if expiryTime.Before(wq.items[tmid].lastUsedTime()) {
        r = mid - 1
    } else {
        l = mid + 1
    }
    

    在二分查找的循环中,方法计算中间位置的逻辑索引,并将其映射回实际的队列索引。然后比较中间位置任务的最后使用时间与过期时间,调整查找范围。如果中间任务的时间早于过期时间,则移动左边界,否则移动右边界。

    在这一步中,我们常遇到的问题也就是左开右闭和左闭右开带来的常规问题了。

    往往在迭代问题中,我们将问题拆解,将复杂问题转换为:

    • 确定边界
    • 确定迭代策略
  4. 返回索引位置

    return (r + basel + nlen) % nlen
    

同步机制

在展开这部分之前,我们首先要阐明一下什么是同步机制、同步机制在这里具体是指什么。其实所谓的同步机制,就是指确保安全并发访问池组件之间的信号同步

  1. 锁机制 Spinlock

    ants 库使用自定义的自旋锁实现,而不是标准 sync.Mutex ,以在高冲突场景中提高性能。在 Pool 中主要使用锁来保护对工作队列的并发安全。

    Syntax error in textmermaid version 10.9.1
    ERROR: [Mermaid] Parse error on line 4: ...ction TBclass sync.Locker { <<interfa ---------------------^ Expecting 'NEWLINE', 'EOF', 'SQS', 'STR', 'GENERICTYPE', 'LABEL', 'STRUCT_START', 'STRUCT_STOP', 'STYLE_SEPARATOR', 'ANNOTATION_END', 'AGGREGATION', 'EXTENSION', 'COMPOSITION', 'DEPENDENCY', 'LOLLIPOP', 'LINE', 'DOTTED_LINE', 'CALLBACK_NAME', 'HREF', 'ALPHA', 'NUM', 'MINUS', 'UNICODE_TEXT', 'BQUOTE_STR', got 'DOT'
    
    • 核心流程

      Success
      Failure
      Yes
      No
      Unlock
      Lock Released
      Start Unlock
      Atomic Store
      value = 0
      Start Lock
      Try CAS
      01
      Lock Acquired
      Backoff
      initial = 1
      Yield CPU scheduler
      runtime.Gosched
      Backoff < maxBackoff
      Double backoff
      Keep current backoff
      
    • 源码走读

      package sync
      
      import (
      	"runtime"
      	"sync"
      	"sync/atomic"
      )
      
      type spinLock uint32
      
      const maxBackoff = 16
      
      func (sl *spinLock) Lock() {
      	backoff := 1
      	for !atomic.CompareAndSwapUint32((*uint32)(sl), 0, 1) {
      		// Leverage the exponential backoff algorithm, see https://en.wikipedia.org/wiki/Exponential_backoff.
      		for i := 0; i < backoff; i++ {
      			runtime.Gosched()
      		}
      		if backoff < maxBackoff {
      			backoff <<= 1
      		}
      	}
      }
      
      func (sl *spinLock) Unlock() {
      	atomic.StoreUint32((*uint32)(sl), 0)
      }
      
      // NewSpinLock instantiates a spin-lock.
      func NewSpinLock() sync.Locker {
      	return new(spinLock)
      }
      
      • Lock()

        for !atomic.CompareAndSwapUint32((*uint32)(sl), 0, 1) {
            for i := 0; i < backoff; i++ {
                runtime.Gosched()
            }
            if backoff < maxBackoff {
                backoff <<= 1
            }
        }
        
        • 使用 atomic.CompareAndSwapUint32 尝试将锁的状态从 0(未锁定)设置为 1(已锁定)
        • 如果获取失败,进入自旋等待。自旋过程中,采用指数退避算法逐步增加等待时间,以减少对 CPU 的占用
        • 在自旋等待中,runtime.Gosched() 会让出当前协程的执行权,允许其他协程运行,从而避免长时间占用 CPU。
      • UnLock()

        atomic.StoreUint32((*uint32)(sl), 0)
        
        • 通过 atomic.StoreUint32 将锁的状态重置为 0,表示锁已释放

    Exponential backoff  指数退避

    指数退避是一种算法,它使用反馈来乘性降低某些过程的速率,以逐渐找到可接受的速率。这些算法在广泛的系统和过程中得到应用,尤其是在无线网络和计算机网络中尤为突出。

    指数退避算法是一种闭环控制系统,它根据不利事件降低受控过程的速率。例如,如果智能手机应用程序无法连接到其服务器,它可能会在 1 秒后再次尝试,如果再次失败,则会在 2 秒后尝试,然后是 4 秒,等等。每次暂停都会乘以一个固定的量(在这种情况下是 2)。在这种情况下,不利事件是连接到服务器的失败。其他不利事件示例包括网络流量的冲突、来自服务的错误响应或显式请求降低速率(即退避)。

    速率降低可以建模为指数函数:

    t=bct = b^c

    或者

    f=1bcf = \frac{1}{b^c}

    在这里,t 是动作之间的时间延迟,b 是乘法因子或基数,c 是观察到的负面事件数量,f 是过程频率(或速率)(即单位时间内的动作数量)。每当观察到负面事件时,c 的值就会增加,从而导致延迟呈指数增长,因此速率成反比。b = 2 的指数退避算法被称为二进制指数退避算法。

    • 基准测试

      Lock TypePerformance (ns/op)
      Mutex  互斥锁111.1
      Original Spinlock  原始自旋锁18.01
      Backoff Spinlock  回退自旋锁10.81
  2. 原子操作

    FieldTypePurposeOperations
    capacityint32Pool maximum sizeLoadInt32StoreInt32
    runningint32Active worker countAddInt32LoadInt32
    stateint32Pool state (OPENED/CLOSED)LoadInt32CompareAndSwapInt32
    waitingint32Blocked submitters countAddInt32LoadInt32
    purgeDoneint32Purge goroutine stateStoreInt32
    ticktockDoneint32Ticktock goroutine stateStoreInt32
    nowatomic.ValueCached current timeStoreLoad
  3. 信号同步

    • 获取 worker

      // retrieveWorker returns an available worker to run the tasks.
      func (p *poolCommon) retrieveWorker() (w worker, err error) {
      	p.lock.Lock()
      
      retry:
      	// First try to fetch the worker from the queue.
      	if w = p.workers.detach(); w != nil {
      		p.lock.Unlock()
      		return
      	}
      
      	// If the worker queue is empty, and we don't run out of the pool capacity,
      	// then just spawn a new worker goroutine.
      	if capacity := p.Cap(); capacity == -1 || capacity > p.Running() {
      		p.lock.Unlock()
      		w = p.workerCache.Get().(worker)
      		w.run()
      		return
      	}
      
      	// Bail out early if it's in nonblocking mode or the number of pending callers reaches the maximum limit value.
      	if p.options.Nonblocking || (p.options.MaxBlockingTasks != 0 && p.Waiting() >= p.options.MaxBlockingTasks) {
      		p.lock.Unlock()
      		return nil, ErrPoolOverload
      	}
      
      	// Otherwise, we'll have to keep them blocked and wait for at least one worker to be put back into pool.
      	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
      }
      
      • 通过加锁机制保护共享资源,防止并发访问问题。

      • 进入 retry 标签后,尝试从 workers 队列中分离出一个可用的 worker。如果成功获取,则解锁并返回该 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().(worker)
            w.run()
            return
        }
        
      • 在非阻塞模式下,或者当等待的任务数已达到最大限制时,方法会直接返回错误 ErrPoolOverload,表示无法获取 worker

        if p.options.Nonblocking || (p.options.MaxBlockingTasks != 0 && p.Waiting() >= p.options.MaxBlockingTasks) {
            p.lock.Unlock()
            return nil, ErrPoolOverload
        }
        
      • 如果以上条件均不满足,方法会将当前调用者标记为等待状态,并通过 p.cond.Wait() 阻塞,直到有新的 worker 被放回池中。解锁前会检查池是否已关闭,若关闭则返回 ErrPoolClosed

        p.addWaiting(1)
        p.cond.Wait()
        p.addWaiting(-1)
        
        if p.IsClosed() {
            p.lock.Unlock()
            return nil, ErrPoolClosed
        }
        
      • 通过 goto retry 标签重新尝试获取 worker,确保在资源可用时能够成功分配。

    • 释放 worker

      // revertWorker puts a worker back into free pool, recycling the goroutines.
      func (p *poolCommon) revertWorker(worker worker) bool {
      	if capacity := p.Cap(); (capacity > 0 && p.Running() > capacity) || p.IsClosed() {
      		p.cond.Broadcast()
      		return false
      	}
      
      	worker.setLastUsedTime(p.nowTime())
      
      	p.lock.Lock()
      	// To avoid memory leaks, add a double check in the lock scope.
      	// Issue: https://github.com/panjf2000/ants/issues/113
      	if p.IsClosed() {
      		p.lock.Unlock()
      		return false
      	}
      	if err := p.workers.insert(worker); err != nil {
      		p.lock.Unlock()
      		return false
      	}
      	// Notify the invoker stuck in 'retrieveWorker()' of there is an available worker in the worker queue.
      	p.cond.Signal()
      	p.lock.Unlock()
      
      	return true
      }
      
      • 检查当前池的容量是否已满,或者池是否已关闭。如果满足这些条件,则通过 p.cond.Broadcast() 唤醒可能等待的调用者,并返回 false,表示无法将该 worker 放回池中

        if capacity := p.Cap(); (capacity > 0 && p.Running() > capacity) || p.IsClosed() {
            p.cond.Broadcast()
            return false
        }
        
      • 更新该 worker 的最后使用时间为当前时间,以便后续清理过期 worker 时使用

        worker.setLastUsedTime(p.nowTime())
        
      • 加锁以保护共享资源,避免并发问题。

      • 在锁的作用域内,再次检查池是否已关闭。如果池已关闭,则解锁并返回 false,防止内存泄漏

        if p.IsClosed() {
        	p.lock.Unlock()
        	return false
        }
        
      • 通知卡在 retrieveWorker() 中的调用者,在 worker 队列中有一个可用的 worker

        	p.cond.Signal()
        
操作同步机制
创建锁和条件变量的常规初始化
任务提交锁、原子计数器、条件变量
工作者复用队列访问锁、状态原子操作
池大小调整容量原子更新、条件广播
池关闭状态原子变更、上下文取消、条件广播
池重启状态原子变更、上下文重建

调优策略

初始参数

  1. 池容量

    1. CPU 密集型任务:将池大小设置为接近可用 CPU 核心数,以最小化上下文切换。
    2. I/O 密集型任务:将池大小设置为高于 CPU 核心数,因为 goroutines 将花费时间等待 I/O 操作。
    3. 混合工作负载:从池大小为 CPU 核心数的 2 倍开始,并根据监控/压测结果进行调整。
  2. 生命周期

    • 短时程:减少内存使用,但增加工作进程创建/销毁开销
    • 持续时间更长:增加内存使用,但减少工作进程创建/销毁开销
  3. 内存管理策略

    • 启用预分配:减少操作期间的内存分配开销,适用于高吞吐量场景
    • 禁用预分配:节省初始内存使用,更适合可能无法充分利用其容量的池
Workload TypePool SizePreAllocExpiryDurationNonblockingOther Considerations
CPU-bound≈ GOMAXPROCStrueLongerfalseConsider CPU affinity
I/O-bound> GOMAXPROCSfalseMediumDependsMonitor blocking I/O
BurstyStart smallfalseShortertrueUse Tune() for peaks
Stable, high-volumeLargertrueLong or disablefalseConsider WithMaxBlockingTasks
Memory-constrainedSmallerfalseShortertrueMonitor memory usage
Latency-sensitiveMediumtrueLong or disabletrueMinimize garbage collection

Example Configurations

  • High-Throughput System

    p, _ := ants.NewPool(
        runtime.GOMAXPROCS(0) * 8,
        ants.WithPreAlloc(true),
        ants.WithDisablePurge(true),
        ants.WithMaxBlockingTasks(50000),
    )
    
  • Low-Latency System

    p, _ := ants.NewPool(
        runtime.GOMAXPROCS(0) * 4,
        ants.WithPreAlloc(true),
        ants.WithNonblocking(true),
        ants.WithDisablePurge(true),
    )
    
  • Memory-Constrained Environment

    p, _ := ants.NewPool(
        runtime.GOMAXPROCS(0),
        ants.WithExpiryDuration(100 * time.Millisecond),
        ants.WithMaxBlockingTasks(100),
    )
    

监控指标

  1. 运行中的工作者:使用 pool.Running()  跟踪活动工作者的数量
  2. 空闲工作者:使用 pool.Free()  监控可用容量
  3. 等待时间:跟踪任务执行前的等待时长
  4. 完成时间:衡量任务完成所需的时间
  5. 错误率:监控提交错误,特别是 ErrPoolOverload

迭代策略

  1. 根据工作负载类型从合理的默认值开始
  2. 使用现实的工作负载进行基准测试
  3. 一次调整一个参数并测量影响
  4. 在生产中监控并根据需要调整