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

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

Go#Go

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

快速开始

安装

Terminal window
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 减少内存分配开销
  • 提供受控的并发机制
  • 提升高吞吐量场景下的性能
classDiagram
	direction TB
class poolCommon {
    +int32 capacity
    +int32 running
    +sync.Locker lock
    +workerQueue workers
    +int32 state
    +sync.Cond cond
    +channel 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
}

class Pool {
    +Submit(task func()) error
}

Pool --|> poolCommon : embeds

class 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()
}

class Pool {
	+ Submit(task func()) error
	+ Running() int
	+ Free() int
	+ Waiting() int
	+ Cap() int
	+ Tune(size int)
	+ ReleaseTimeout(timeout time.Duration) error
	+ Reboot()
}

MultiPool --> Pool : contains multiple

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
stateDiagram-v2
    [*] --> NewPool
    NewPool --> Created
    Created --> Running : Submit tasks
    Running --> Running : Submit more tasks
    Running --> Released : Release()
    Released --> [*] : Resources freed
    Released --> Rebooted : Reboot()
    Rebooted --> Running : Submit tasks
  • 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
flowchart TD
    SubmitTask --> CheckStrategy

    CheckStrategy -->|if strategy == RoundRobin| RoundRobin
    RoundRobin --> IncrementCounter[Increment counter and get next pool]
    IncrementCounter --> SelectNextPool
    SelectNextPool --> SubmitToPool

    CheckStrategy -->|if strategy == LeastTasks| LeastTasks
    LeastTasks --> ScanPools[Scan all pools]
    ScanPools --> FindPoolWithFewestTasks
    FindPoolWithFewestTasks --> SubmitToPool

    SubmitToPool --> TrySubmit
    TrySubmit -->|if pool accepts task| Success
    TrySubmit -->|if ErrPoolOverload| Failure

    Success --> End1([★ Success End])

    Failure -->|if already using LeastTasks| ReturnError
    ReturnError --> End2([★ Failure End])

    Failure -->|if RoundRobin was used initially| FallbackStrategy
    FallbackStrategy --> UseFallback[Use LeastTasks as fallback]
    UseFallback --> FindPoolWithFewestTasks

状态流转

stateDiagram-v2
    [*] --> NewPool
    NewPool --> Created
    Created --> Running : worker.run()
    Running --> Processing
    Running --> [*] : Pool closed
    Receive --> Idle : Task completed
    Idle --> [*] : Purged after ExpiryDuration
    Idle --> [*] : Pool closed
    Idle --> Processing

源码解读

创建流程

sequenceDiagram
    participant Caller
    participant ants as ants.NewPool
    participant Common as newPool
    participant Pool as Pool
    participant WorkerCache as pool.workerCache.New

    Caller->>ants: NewPool(size, options...)
    ants->>Common: newPool(size, options...)
    Common-->>ants: poolCommon, err
    alt if err == nil
        ants->>Pool: 创建 Pool 实例
        Pool->>WorkerCache: 设置 workerCache.New
        WorkerCache-->>Pool: 返回 goWorker 实例创建函数
    end
    ants-->>Caller: *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(),选择任务数最少的池的索引
flowchart TD
    Start((●)) --> SubmitTask[SubmitTask]
    SubmitTask --> CheckStrategy[CheckStrategy]
    
    CheckStrategy -->|if strategy == RoundRobin| RoundRobin[RoundRobin]
    CheckStrategy -->|if strategy == LeastTasks| LeastTasks[LeastTasks]
    
    RoundRobin --> IncrementCounter["Increment counter and get\nnext pool"]
    IncrementCounter --> SelectNextPool[SelectNextPool]
    SelectNextPool --> SubmitToPool[SubmitToPool]
    
    LeastTasks --> ScanAllPools["Scan all pools"]
    ScanAllPools --> FindPoolWithFewestTasks[FindPoolWithFewestTasks]
    FindPoolWithFewestTasks --> SubmitToPool
    
    SubmitToPool --> TrySubmit[TrySubmit]
    
    TrySubmit -->|if ErrPoolOverload| Failure[Failure]
    TrySubmit -->|if pool accepts task| Success[Success]
    
    Failure -->|if RoundRobin was used\ninitially| FallbackStrategy[FallbackStrategy]
    Failure -->|if already using LeastTasks| ReturnError[ReturnError]
    
    FallbackStrategy --> UseLeastTasks["Use LeastTasks as fallback"]
    UseLeastTasks --> FindPoolWithFewestTasks
    
    Success --> End((●))
    Success --> |retry| TrySubmit

工作队列

classDiagram
	direction TB
	
class Worker {
	<<interface>>
	-len() int
	-isEmpty() bool
	-insert(worker) error
	-detach() worker
	-refresh(time.Duration) []worker
	-reset()
}

class workerStack {
	-items []worker
	-expiry []worker
}

class workerLoopQueue {
	-items []worker
	-expiry []worker
	-head int
	-tail int
	-size int
	-isFull bool
}

Worker <|.. workerStack : implements
Worker <|.. workerLoopQueue : implements
  • 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 中主要使用锁来保护对工作队列的并发安全。

    classDiagram
    	direction TB
    
    class Locker {
    	<<interface>>
    	+Lock()
    	+Unlock()
    }
    
    class spinLock {
    	-uint32 value
    }
    
    Locker <|.. spinLock : implements
    
    • 核心流程

      flowchart TD
          A[Start Lock] --> B{Try CAS
          0 → 1}
          B -- Success --> C[Lock Acquired]
          B -- Failure --> D[Backoff
          initial = 1]
          D --> E[Yield CPU scheduler
          runtime.Gosched]
          E --> F{Backoff < maxBackoff}
          F -- Yes --> G[Double backoff]
          G --> B
          F -- No --> H[Keep current backoff]
          H --> B
      
          subgraph Unlock
              U[Start Unlock] --> V[Atomic Store
              value = 0] --> W[Lock Released]
          end
      
    • 源码走读

      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 = b^c $$

    或者

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

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

    • 基准测试

      Lock Type Performance (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. 信号同步

    sequenceDiagram
        participant Client
        participant Pool
        participant sync.Cond
        participant WorkerQueue as "Worker Queue"
    
        Client->>Pool: Submit(task)
        Pool->>Pool: retrieveWorker()
        Pool->>WorkerQueue: detach()
        WorkerQueue-->>Pool: nil (no workers)
            alt Pool at capacity
                Pool->>Pool: addWaiting(+1)
                Pool->>sync.Cond: Wait()
                Note right of sync.Cond: Block until worker available
                loop Until worker available or pool closed
                    Pool-->>sync.Cond: (blocked)
                end
                Pool->>Pool: addWaiting(-1)
                Pool->>WorkerQueue: detach()
                WorkerQueue-->>Pool: worker
            end
       Pool-->>Client: Success/Error
    
    • 获取 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. 在生产中监控并根据需要调整