簡介
繼上一篇Go 每日一庫之 ants,這篇文章我們來一起看看ants
的源碼。
Pool
通過上篇文章,我們知道ants
池有兩種創(chuàng)建方式:
-
p, _ := ants.NewPool(cap)
:這種方式創(chuàng)建的池子對(duì)象需要調(diào)用p.Submit(task)
提交任務(wù),任務(wù)是一個(gè)無參數(shù)無返回值的函數(shù); -
p, _ := ants.NewPoolWithFunc(cap, func(interface{}))
:這種方式創(chuàng)建的池子對(duì)象需要指定池函數(shù),并且使用p.Invoke(arg)
調(diào)用池函數(shù)。arg
就是傳給池函數(shù)func(interface{})
的參數(shù)。
在ants
中這兩種池子使用不同的結(jié)構(gòu)來表示:ants.Pool
和ants.PoolWithFunc
。我們先來介紹Pool
。PoolWithFunc
結(jié)構(gòu)也是類似的,介紹完Pool
之后,我們?cè)俸唵伪容^一下它們。
Pool
結(jié)構(gòu)定義在文件pool.go
中:
// src/github.com/panjf2000/ants/pool.go
type Pool struct {
capacity int32
running int32
workers workerArray
state int32
lock sync.Locker
cond *sync.Cond
workerCache sync.Pool
blockingNum int
options *Options
}
各個(gè)字段含義如下:
-
capacity
:池容量,表示ants
最多能創(chuàng)建的 goroutine 數(shù)量。如果為負(fù)數(shù),表示容量無限制; -
running
:已經(jīng)創(chuàng)建的 worker goroutine 的數(shù)量; -
workers
:存放一組 worker 對(duì)象,workerArray
只是一個(gè)接口,表示一個(gè) worker 容器,后面詳述; -
state
:記錄池子當(dāng)前的狀態(tài),是否已關(guān)閉(CLOSED
); -
lock
:鎖。ants
自己實(shí)現(xiàn)了一個(gè)自旋鎖。用于同步并發(fā)操作; -
cond
:條件變量。處理任務(wù)等待和喚醒; -
workerCache
:使用sync.Pool
對(duì)象池管理和創(chuàng)建worker
對(duì)象,提升性能; -
blockingNum
:阻塞等待的任務(wù)數(shù)量; -
options
:選項(xiàng)。上一篇文章已經(jīng)詳細(xì)介紹過了。
這里明確一個(gè)概念,ants
中為每個(gè)任務(wù)都是由 worker 對(duì)象來處理的,每個(gè) worker 對(duì)象會(huì)對(duì)應(yīng)創(chuàng)建一個(gè) goroutine 來處理任務(wù)。ants
中使用goWorker
表示 worker:
// src/github.com/panjf2000/ants/worker.go
type goWorker struct {
pool *Pool
task chan func()
recycleTime time.Time
}
后文詳細(xì)介紹這一塊內(nèi)容,現(xiàn)在我們只需要知道Pool.workers
字段就是存放goWorker
對(duì)象的容器。
Pool
創(chuàng)建
創(chuàng)建Pool
對(duì)象需調(diào)用ants.NewPool(size, options)
函數(shù)。省略了一些處理選項(xiàng)的代碼,最終代碼如下:
// src/github.com/panjf2000/ants/pool.go
func NewPool(size int, options ...Option) (*Pool, error) {
// ...
p := &Pool{
capacity: int32(size),
lock: internal.NewSpinLock(),
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 = newWorkerArray(loopQueueType, size)
} else {
p.workers = newWorkerArray(stackType, 0)
}
p.cond = sync.NewCond(p.lock)
go p.purgePeriodically()
return p, nil
}
代碼不難理解:
- 創(chuàng)建
Pool
對(duì)象,設(shè)置容量,創(chuàng)建一個(gè)自旋鎖來初始化lock
字段,設(shè)置選項(xiàng); - 設(shè)置
workerCache
這個(gè)sync.Pool
對(duì)象的New
方法,在調(diào)用sync.Pool
對(duì)象的Get()
方法時(shí),如果它沒有緩存的 worker 對(duì)象了,則調(diào)用這個(gè)方法創(chuàng)建一個(gè); - 根據(jù)是否設(shè)置了預(yù)分配選項(xiàng),創(chuàng)建不同類型的 workers;
- 使用
p.lock
鎖創(chuàng)建一個(gè)條件變量; - 最后啟動(dòng)一個(gè) goroutine 用于定期清理過期的 worker。
Pool.workers
字段為workerArray
類型,這實(shí)際上是一個(gè)接口,表示一個(gè) worker 容器:
type workerArray interface {
len() int
isEmpty() bool
insert(worker *goWorker) error
detach() *goWorker
retrieveExpiry(duration time.Duration) []*goWorker
reset()
}
每個(gè)方法從名字上很好理解含義:
-
len() int
:worker 數(shù)量; -
isEmpty() bool
:worker 數(shù)量是否為 0; -
insert(worker *goWorker) error
:goroutine 任務(wù)執(zhí)行結(jié)束后,將相應(yīng)的 worker 放回workerArray
中; -
detach() *goWorker
:從workerArray
中取出一個(gè) worker; -
retrieveExpiry(duration time.Duration) []*goWorker
:取出所有的過期 worker; -
reset()
:重置容器。
workerArray
在ants
中有兩種實(shí)現(xiàn),即workerStack
和loopQueue
。
workerStack
我們先來介紹一下workerStack
,它位于文件worker_stack.go
中:
// src/github.com/panjf2000/ants/worker_stack.go
type workerStack struct {
items []*goWorker
expiry []*goWorker
size int
}
func newWorkerStack(size int) *workerStack {
return &workerStack{
items: make([]*goWorker, 0, size),
size: size,
}
}
-
items
:空閑的worker
; -
expiry
:過期的worker
。
goroutine 完成任務(wù)之后,Pool
池會(huì)將相應(yīng)的 worker 放回workerStack
,調(diào)用workerStack.insert()
直接append
到items
中即可:
func (wq *workerStack) insert(worker *goWorker) error {
wq.items = append(wq.items, worker)
return nil
}
新任務(wù)到來時(shí),會(huì)調(diào)用workerStack.detach()
從容器中取出一個(gè)空閑的 worker:
func (wq *workerStack) detach() *goWorker {
l := wq.len()
if l == 0 {
return nil
}
w := wq.items[l-1]
wq.items[l-1] = nil // avoid memory leaks
wq.items = wq.items[:l-1]
return w
}
這里總是返回最后一個(gè) worker,每次insert()
也是append
到最后,符合棧后進(jìn)先出的特點(diǎn),故稱為workerStack
。
這里有一個(gè)細(xì)節(jié),由于切片的底層結(jié)構(gòu)是數(shù)組,只要有引用數(shù)組的指針,數(shù)組中的元素就不會(huì)釋放。這里取出切片最后一個(gè)元素后,將對(duì)應(yīng)數(shù)組元素的指針設(shè)置為nil
,主動(dòng)釋放這個(gè)引用。
上面說過新建Pool
對(duì)象時(shí)會(huì)創(chuàng)建一個(gè) goroutine 定期檢查和清理過期的 worker。通過調(diào)用workerArray.retrieveExpiry()
獲取過期的 worker 列表。workerStack
實(shí)現(xiàn)如下:
func (wq *workerStack) retrieveExpiry(duration time.Duration) []*goWorker {
n := wq.len()
if n == 0 {
return nil
}
expiryTime := time.Now().Add(-duration)
index := wq.binarySearch(0, n-1, expiryTime)
wq.expiry = wq.expiry[:0]
if index != -1 {
wq.expiry = append(wq.expiry, wq.items[:index+1]...)
m := copy(wq.items, wq.items[index+1:])
for i := m; i < n; i++ {
wq.items[i] = nil
}
wq.items = wq.items[:m]
}
return wq.expiry
}
實(shí)現(xiàn)使用二分查找法找到已過期的最近一個(gè) worker。由于過期時(shí)間是按照 goroutine 執(zhí)行任務(wù)后的空閑時(shí)間計(jì)算的,而workerStack.insert()
入隊(duì)順序決定了,它們的過期時(shí)間是從早到晚的。所以可以使用二分查找:
func (wq *workerStack) binarySearch(l, r int, expiryTime time.Time) int {
var mid int
for l <= r {
mid = (l + r) / 2
if expiryTime.Before(wq.items[mid].recycleTime) {
r = mid - 1
} else {
l = mid + 1
}
}
return r
}
二分查找的是最近過期的 worker,即將過期的 worker 的前一個(gè)。它和在它之前的 worker 已經(jīng)全部過期了。
如果找到索引index
,將items
從開頭到index
(包括)的所有 worker 復(fù)制到expiry
字段中。然后將index
之后的所有未過期 worker 復(fù)制到切片頭部,這里使用了copy
函數(shù)。copy
返回實(shí)際復(fù)制的數(shù)量,即未過期的 worker 數(shù)量m
。然后將切片items
從m
開始所有的元素置為nil
,避免內(nèi)存泄漏,因?yàn)樗鼈円呀?jīng)被復(fù)制到頭部了。最后裁剪items
切片,返回過期 worker 切片。
loopQueue
loopQueue
實(shí)現(xiàn)基于循環(huán)隊(duì)列,結(jié)構(gòu)定義在文件worker_loop_queue
中:
type loopQueue struct {
items []*goWorker
expiry []*goWorker
head int
tail int
size int
isFull bool
}
func newWorkerLoopQueue(size int) *loopQueue {
return &loopQueue{
items: make([]*goWorker, size),
size: size,
}
}
由于是循環(huán)隊(duì)列,這里先創(chuàng)建好了一個(gè)長度為size
的切片。循環(huán)隊(duì)列有一個(gè)隊(duì)列頭指針head
,指向第一個(gè)有元素的位置,一個(gè)隊(duì)列尾指針tail
,指向下一個(gè)可以存放元素的位置。所以一開始狀態(tài)如下:
[圖片上傳失敗...(image-77c78a-1623799706182)]
在tail
處添加元素,添加后tail
指針后移。在head
處取出元素,取出后head
指針也后移。進(jìn)行一段時(shí)間操作后,隊(duì)列狀態(tài)如下:
[圖片上傳失敗...(image-f3a5af-1623799706182)]
head
或tail
指針到隊(duì)列尾了,需要回繞。所以可能出現(xiàn)這種情況:
[圖片上傳失敗...(image-a85640-1623799706182)]
當(dāng)tail
指針趕上head
指針了,說明隊(duì)列就滿了:
[圖片上傳失敗...(image-f2cadd-1623799706182)]
當(dāng)head
指針趕上tail
指針了,隊(duì)列再次為空:
[圖片上傳失敗...(image-96e24d-1623799706182)]
根據(jù)示意圖,我們?cè)賮砜?code>loopQueue的操作方法就很簡單了。
由于head
和tail
相等的情況有可能是隊(duì)列空,也有可能是隊(duì)列滿,所以loopQueue
中增加一個(gè)isFull
字段以示區(qū)分。goroutine 完成任務(wù)之后,會(huì)將對(duì)應(yīng)的 worker 對(duì)象放回loopQueue
,執(zhí)行的是insert()
方法:
func (wq *loopQueue) insert(worker *goWorker) error {
if wq.size == 0 {
return errQueueIsReleased
}
if wq.isFull {
return errQueueIsFull
}
wq.items[wq.tail] = worker
wq.tail++
if wq.tail == wq.size {
wq.tail = 0
}
if wq.tail == wq.head {
wq.isFull = true
}
return nil
}
這個(gè)方法執(zhí)行的就是循環(huán)隊(duì)列的入隊(duì)流程,注意如果插入后tail==head
了,說明隊(duì)列滿了,設(shè)置isFull
字段。
新任務(wù)到來調(diào)用loopQueeue.detach()
方法獲取一個(gè)空閑的 worker 結(jié)構(gòu):
func (wq *loopQueue) detach() *goWorker {
if wq.isEmpty() {
return nil
}
w := wq.items[wq.head]
wq.items[wq.head] = nil
wq.head++
if wq.head == wq.size {
wq.head = 0
}
wq.isFull = false
return w
}
這個(gè)方法對(duì)應(yīng)的是循環(huán)隊(duì)列的出隊(duì)流程,注意每次出隊(duì)后,隊(duì)列肯定不滿了,isFull
要重置為false
。
與workerStack
結(jié)構(gòu)一樣,先入的 worker 對(duì)象過期時(shí)間早,后入的晚,獲取過期 worker 的方法與workerStack
中類似,只是沒有使用二分查找了。這里就不贅述了。
再看Pool
創(chuàng)建
介紹完兩種workerArray
的實(shí)現(xiàn)之后,再來看Pool
的創(chuàng)建函數(shù)中workers
字段的設(shè)置:
if p.options.PreAlloc {
if size == -1 {
return nil, ErrInvalidPreAllocSize
}
p.workers = newWorkerArray(loopQueueType, size)
} else {
p.workers = newWorkerArray(stackType, 0)
}
newWorkerArray()
定義在文件worker_array.go
中:
type arrayType int
const (
stackType arrayType = 1 << iota
loopQueueType
)
func newWorkerArray(aType arrayType, size int) workerArray {
switch aType {
case stackType:
return newWorkerStack(size)
case loopQueueType:
return newWorkerLoopQueue(size)
default:
return newWorkerStack(size)
}
}
即如果設(shè)置了預(yù)分配選項(xiàng),就采用loopQueue
結(jié)構(gòu)。否則就采用stack
的結(jié)構(gòu)。
worker 結(jié)構(gòu)
介紹完Pool
的創(chuàng)建和結(jié)構(gòu),我們來看看 worker 的結(jié)構(gòu)。在ants
中 worker 用結(jié)構(gòu)體goWorker
表示,定義在文件worker.go
中。它的結(jié)構(gòu)非常簡單:
// src/github.com/panjf2000/ants/worker.go
type goWorker struct {
pool *Pool
task chan func()
recycleTime time.Time
}
具體字段含義很明顯:
-
pool
:持有 goroutine 池的引用; -
task
:任務(wù)通道,通過這個(gè)通道將類型為func ()
的函數(shù)作為任務(wù)發(fā)送給goWorker
; -
recyleTime
:這個(gè)字段記錄goWorker
什么時(shí)候被放回池中(即什么時(shí)候開始空閑)。其完成任務(wù)后,在將其放回 goroutine 池的時(shí)候設(shè)置。
goWorker
創(chuàng)建時(shí)會(huì)調(diào)用run()
方法,run()
方法中啟動(dòng)一個(gè)新 goroutine 處理任務(wù)。run()
主體流程非常簡單:
func (w *goWorker) run() {
go func() {
for f := range w.task {
if f == nil {
return
}
f()
if ok := w.pool.revertWorker(w); !ok {
return
}
}
}()
}
這個(gè)方法啟動(dòng)一個(gè)新的 goroutine,然后不停地從task
通道中接收任務(wù),然后執(zhí)行任務(wù),任務(wù)執(zhí)行完成之后調(diào)用池對(duì)象的revertWorker()
方法將該goWorker
對(duì)象放回池中,以便下次取出處理新的任務(wù)。revertWorker()
方法后面會(huì)詳細(xì)分析。
這里注意,實(shí)際上for f := range w.task
這個(gè)循環(huán)直到通道task
關(guān)閉或取出為nil
的任務(wù)才會(huì)終止。所以這個(gè) goroutine 一直在運(yùn)行,這正是ants
高性能的關(guān)鍵所在。每個(gè)goWorker
只會(huì)啟動(dòng)一次 goroutine, 后續(xù)重復(fù)利用這個(gè) goroutine。goroutine 每次只執(zhí)行一個(gè)任務(wù)就會(huì)被放回池中。
還有一個(gè)細(xì)節(jié),如果放回操作失敗,則會(huì)調(diào)用return
,這會(huì)讓 goroutine 運(yùn)行結(jié)束,防止 goroutine 泄漏。
這里f == nil
為 true 時(shí)return
,也是一個(gè)細(xì)節(jié)點(diǎn),我們后面講池關(guān)閉的時(shí)候會(huì)詳細(xì)介紹。
下面我們看看run()
方法的異常處理:
defer func() {
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 a panic: %v\n", p)
var buf [4096]byte
n := runtime.Stack(buf[:], false)
w.pool.options.Logger.Printf("worker exits from panic: %s\n", string(buf[:n]))
}
}
w.pool.cond.Signal()
}()
簡單來說,就是在defer
中通過recover()
函數(shù)捕獲任務(wù)執(zhí)行過程中拋出的panic
。這時(shí)任務(wù)執(zhí)行失敗,goroutine 也結(jié)束了。但是goWorker
對(duì)象還是可以重復(fù)利用,所以defer
函數(shù)一開始調(diào)用w.pool.workerCache.Put(w)
將goWorker
對(duì)象放回sync.Pool
池中。
接著就是處理panic
,如果選項(xiàng)中指定了panic
處理器,直接調(diào)用這個(gè)處理器。否則,ants
調(diào)用選項(xiàng)中設(shè)置的Logger
記錄一些日志,如堆棧,panic
信息等。
最后需要調(diào)用w.pool.cond.Signal()
通知現(xiàn)在有空閑的goWorker
了。因?yàn)槲覀儗?shí)際運(yùn)行的goWorker
數(shù)量由于panic
少了一個(gè),而池中可能有其他任務(wù)在等待處理。
提交任務(wù)
接下來,通過提交任務(wù)就可以串起整個(gè)流程。由上一篇文章我們知道,可以調(diào)用池對(duì)象的Submit()
方法提交任務(wù):
func (p *Pool) Submit(task func()) error {
if p.IsClosed() {
return ErrPoolClosed
}
var w *goWorker
if w = p.retrieveWorker(); w == nil {
return ErrPoolOverload
}
w.task <- task
return nil
}
首先判斷池是否已關(guān)閉,然后調(diào)用retrieveWorker()
方法獲取一個(gè)空閑的 worker,然后將任務(wù)task
發(fā)送到 worker 的任務(wù)通道。下面是retrieveWorker()
實(shí)現(xiàn):
func (p *Pool) retrieveWorker() (w *goWorker) {
p.lock.Lock()
w = p.workers.detach()
if w != nil {
p.lock.Unlock()
} else if capacity := p.Cap(); capacity == -1 || capacity > p.Running() {
p.lock.Unlock()
spawnWorker()
} else {
if p.options.Nonblocking {
p.lock.Unlock()
return
}
Reentry:
if p.options.MaxBlockingTasks != 0 && p.blockingNum >= p.options.MaxBlockingTasks {
p.lock.Unlock()
return
}
p.blockingNum++
p.cond.Wait()
p.blockingNum--
var nw int
if nw = p.Running(); nw == 0 {
p.lock.Unlock()
if !p.IsClosed() {
spawnWorker()
}
return
}
if w = p.workers.detach(); w == nil {
if nw < capacity {
p.lock.Unlock()
spawnWorker()
return
}
goto Reentry
}
p.lock.Unlock()
}
return
}
這個(gè)方法稍微有點(diǎn)復(fù)雜,我們一點(diǎn)點(diǎn)來看。首先調(diào)用p.workers.detach()
獲取goWorker
對(duì)象。p.workers
是loopQueue
或者workerStack
對(duì)象,它們都實(shí)現(xiàn)了detach()
方法,前面已經(jīng)介紹過了。
如果返回了一個(gè)goWorker
對(duì)象,說明有空閑 goroutine,直接返回。
否則,池容量還沒用完(即容量大于正在工作的goWorker
數(shù)量),則調(diào)用spawnWorker()
新建一個(gè)goWorker
,執(zhí)行其run()
方法:
spawnWorker := func() {
w = p.workerCache.Get().(*goWorker)
w.run()
}
否則,池容量已用完。如果設(shè)置了非阻塞選項(xiàng),則直接返回。否則,如果設(shè)置了最大阻塞隊(duì)列長度上限,且當(dāng)前阻塞等待的任務(wù)數(shù)量已經(jīng)達(dá)到這個(gè)上限,直接返回。否則,阻塞等待數(shù)量 +1,調(diào)用p.cond.Wait()
等待。
然后goWorker.run()
完成一個(gè)任務(wù)后,調(diào)用池的revertWorker()
方法放回goWorker
:
func (p *Pool) revertWorker(worker *goWorker) bool {
if capacity := p.Cap(); (capacity > 0 && p.Running() > capacity) || p.IsClosed() {
return false
}
worker.recycleTime = time.Now()
p.lock.Lock()
if p.IsClosed() {
p.lock.Unlock()
return false
}
err := p.workers.insert(worker)
if err != nil {
p.lock.Unlock()
return false
}
p.cond.Signal()
p.lock.Unlock()
return true
}
這里設(shè)置了goWorker
的recycleTime
字段,用于判定過期。然后將goWorker
放回池。workers
的insert()
方法前面也已經(jīng)分析過了。
接著調(diào)用p.cond.Signal()
喚醒之前retrieveWorker()
方法中的等待。retrieveWorker()
方法繼續(xù)執(zhí)行,阻塞等待數(shù)量 -1,這里判斷當(dāng)前goWorker
的數(shù)量(也即 goroutine 數(shù)量)。如果數(shù)量等于 0,很有可能池子剛剛執(zhí)行了Release()
關(guān)閉,這時(shí)需要判斷池是否處于關(guān)閉狀態(tài),如果是則直接返回。否則,調(diào)用spawnWorker()
創(chuàng)建一個(gè)新的goWorker
并執(zhí)行其run()
方法。
如果當(dāng)前goWorker
數(shù)量不為 0,則調(diào)用p.workers.detach()
取出一個(gè)空閑的goWorker
返回。這個(gè)操作有可能失敗,因?yàn)榭赡芡瑫r(shí)有多個(gè) goroutine 在等待,喚醒的時(shí)候只有部分 goroutine 能獲取到goWorker
。如果失敗了,其容量還未用完,直接創(chuàng)建新的goWorker
,反之重新執(zhí)行阻塞等待邏輯。
這里有很多加鎖和解鎖的邏輯,再加上和信號(hào)量混在一起很難看明白。其實(shí)只需要知道一點(diǎn)就很簡單了,那就是p.cond.Wait()
內(nèi)部會(huì)將當(dāng)前 goroutine 掛起,然后解開它持有的鎖,即會(huì)調(diào)用p.lock.Unlock()
。這也是為什么revertWorker()
中p.lock.Lock()
加鎖能成功的原因。然后p.cond.Signal()
或p.cond.Broadcast()
會(huì)喚醒因?yàn)?code>p.cond.Wait()而掛起的 goroutine,但是需要Signal()/Broadcast()
所在 goroutine 調(diào)用解鎖方法。
最后,放上整體流程圖:
[圖片上傳失敗...(image-90fb37-1623799706182)]
清理過期goWorker
在NewPool()
函數(shù)中會(huì)啟動(dòng)一個(gè) goroutine 定期清理過期的goWorker
:
func (p *Pool) purgePeriodically() {
heartbeat := time.NewTicker(p.options.ExpiryDuration)
defer heartbeat.Stop()
for range heartbeat.C {
if p.IsClosed() {
break
}
p.lock.Lock()
expiredWorkers := p.workers.retrieveExpiry(p.options.ExpiryDuration)
p.lock.Unlock()
for i := range expiredWorkers {
expiredWorkers[i].task <- nil
expiredWorkers[i] = nil
}
if p.Running() == 0 {
p.cond.Broadcast()
}
}
}
如果池子已關(guān)閉,直接退出 goroutine。由選項(xiàng)ExpiryDuration
來設(shè)置清理的間隔,如果沒有設(shè)置該選項(xiàng),采用默認(rèn)值 1s:
// src/github.com/panjf2000/ants/pool.go
func NewPool(size int, options ...Option) (*Pool, error) {
if expiry := opts.ExpiryDuration; expiry < 0 {
return nil, ErrInvalidPoolExpiry
} else if expiry == 0 {
opts.ExpiryDuration = DefaultCleanIntervalTime
}
}
// src/github.com/panjf2000/ants/pool.go
const (
DefaultCleanIntervalTime = time.Second
)
然后就是每個(gè)清理周期,調(diào)用p.workers.retrieveExpiry()
方法,取出過期的goWorker
。因?yàn)橛蛇@些goWorker
啟動(dòng)的 goroutine 還阻塞在通道task
上,所以要向該通道發(fā)送一個(gè)nil
值,而goWorker.run()
方法中接收到一個(gè)值為nil
的任務(wù)會(huì)return
,結(jié)束 goroutine,避免了 goroutine 泄漏。
如果所有goWorker
都被清理掉了,可能這時(shí)還有 goroutine 阻塞在retrieveWorker()
方法中的p.cond.Wait()
上,所以這里需要調(diào)用p.cond.Broadcast()
喚醒這些 goroutine。
容量動(dòng)態(tài)修改
在運(yùn)行過程中,可以動(dòng)態(tài)修改池的容量。調(diào)用p.Tune(size int)
方法:
func (p *Pool) Tune(size int) {
if capacity := p.Cap(); capacity == -1 || size <= 0 || size == capacity || p.options.PreAlloc {
return
}
atomic.StoreInt32(&p.capacity, int32(size))
}
這里只是簡單設(shè)置了一下新的容量,不影響當(dāng)前正在執(zhí)行的goWorker
,而且如果設(shè)置了預(yù)分配選項(xiàng),容量不能再次設(shè)置。
下次執(zhí)行revertWorker()
的時(shí)候就會(huì)以新的容量判斷是否能放回,下次執(zhí)行retrieveWorker()
的時(shí)候也會(huì)以新容量判斷是否能創(chuàng)建新goWorker
。
關(guān)閉和重新啟動(dòng)Pool
使用完成之后,需要關(guān)閉Pool
,避免 goroutine 泄漏。調(diào)用池對(duì)象的Release()
方法關(guān)閉:
func (p *Pool) Release() {
atomic.StoreInt32(&p.state, CLOSED)
p.lock.Lock()
p.workers.reset()
p.lock.Unlock()
p.cond.Broadcast()
}
調(diào)用p.workers.reset()
結(jié)束loopQueue
或wokerStack
中的 goroutine,做一些清理工作,同時(shí)為了防止有 goroutine 阻塞在p.cond.Wait()
上,執(zhí)行一次p.cond.Broadcast()
。
workerStack
與loopQueue
的reset()
基本相同,即發(fā)送nil
到task
通道從而結(jié)束 goroutine,然后重置各個(gè)字段:
// loopQueue 版本
func (wq *loopQueue) reset() {
if wq.isEmpty() {
return
}
Releasing:
if w := wq.detach(); w != nil {
w.task <- nil
goto Releasing
}
wq.items = wq.items[:0]
wq.size = 0
wq.head = 0
wq.tail = 0
}
// stack 版本
func (wq *workerStack) reset() {
for i := 0; i < wq.len(); i++ {
wq.items[i].task <- nil
wq.items[i] = nil
}
wq.items = wq.items[:0]
}
池關(guān)閉后還可以調(diào)用Reboot()
重啟:
func (p *Pool) Reboot() {
if atomic.CompareAndSwapInt32(&p.state, CLOSED, OPENED) {
go p.purgePeriodically()
}
}
由于p.purgePeriodically()
在p.Release()
之后檢測到池關(guān)閉就直接退出了,這里需要重新開啟一個(gè) goroutine 定期清理。
PoolWithFunc
和WorkWithFunc
上一篇文章中我們還介紹了另一種方式創(chuàng)建Pool
,即NewPoolWithFunc()
,指定一個(gè)函數(shù)。后面提交任務(wù)時(shí)調(diào)用p.Invoke()
提供參數(shù)就可以執(zhí)行該函數(shù)了。這種方式創(chuàng)建的 Pool 和 Woker 結(jié)構(gòu)如下:
type PoolWithFunc struct {
workers []*goWorkerWithFunc
poolFunc func(interface{})
}
type goWorkerWithFunc struct {
pool *PoolWithFunc
args chan interface{}
recycleTime time.Time
}
與前面介紹的Pool
和goWorker
大體相似,只是PoolWithFunc
保存了傳入的函數(shù)對(duì)象,使用數(shù)組保存 worker。goWorkerWithFunc
以interface{}
為args
通道的數(shù)據(jù)類型,其實(shí)也好理解,因?yàn)橐呀?jīng)有函數(shù)了,只需要傳入數(shù)據(jù)作為參數(shù)就可以運(yùn)行了:
func (w *goWorkerWithFunc) run() {
go func() {
for args := range w.args {
if args == nil {
return
}
w.pool.poolFunc(args)
if ok := w.pool.revertWorker(w); !ok {
return
}
}
}()
}
從通道接收函數(shù)參數(shù),執(zhí)行池中保存的函數(shù)對(duì)象。
其他細(xì)節(jié)
task
緩沖通道
還記得創(chuàng)建p.workerCache
這個(gè)sync.Pool
對(duì)象的代碼么:
p.workerCache.New = func() interface{} {
return &goWorker{
pool: p,
task: make(chan func(), workerChanCap),
}
}
在sync.Pool
中沒有goWorker
對(duì)象時(shí),調(diào)用New()
方法創(chuàng)建一個(gè),注意到這里創(chuàng)建的task
通道使用workerChanCap
作為容量。這個(gè)變量定義在ants.go
文件中:
var (
// workerChanCap determines whether the channel of a worker should be a buffered channel
// to get the best performance. Inspired by fasthttp at
// https://github.com/valyala/fasthttp/blob/master/workerpool.go#L139
workerChanCap = func() int {
// Use blocking channel if GOMAXPROCS=1.
// This switches context from sender to receiver immediately,
// which results in higher performance (under go1.5 at least).
if runtime.GOMAXPROCS(0) == 1 {
return 0
}
// Use non-blocking workerChan if GOMAXPROCS>1,
// since otherwise the sender might be dragged down if the receiver is CPU-bound.
return 1
}()
)
為了方便對(duì)照,我把注釋也放上來了。ants
參考了著名的 Web 框架fasthttp
的實(shí)現(xiàn)。當(dāng)GOMAXPROCS
為 1 時(shí)(即操作系統(tǒng)線程數(shù)為 1),向通道task
發(fā)送會(huì)掛起發(fā)送 goroutine,將執(zhí)行流程轉(zhuǎn)向接收 goroutine,這能提升接收處理性能。如果GOMAXPROCS
大于 1,ants
使用帶緩沖的通道,為了防止接收 goroutine 是 CPU 密集的,導(dǎo)致發(fā)送 goroutine 被阻塞。下面是fasthttp
中的相關(guān)代碼:
// src/github.com/valyala/fasthttp/workerpool.go
var workerChanCap = func() int {
// Use blocking workerChan if GOMAXPROCS=1.
// This immediately switches Serve to WorkerFunc, which results
// in higher performance (under go1.5 at least).
if runtime.GOMAXPROCS(0) == 1 {
return 0
}
// Use non-blocking workerChan if GOMAXPROCS>1,
// since otherwise the Serve caller (Acceptor) may lag accepting
// new connections if WorkerFunc is CPU-bound.
return 1
}()
自旋鎖
ants
利用atomic.CompareAndSwapUint32()
這個(gè)原子操作實(shí)現(xiàn)了一個(gè)自旋鎖。與其他類型的鎖不同,自旋鎖在加鎖失敗之后不會(huì)立刻進(jìn)入等待,而是會(huì)繼續(xù)嘗試。這對(duì)于很快就能獲得鎖的應(yīng)用來說能極大提升性能,因?yàn)槟鼙苊饧渔i和解鎖導(dǎo)致的線程切換:
type spinLock uint32
func (sl *spinLock) Lock() {
backoff := 1
for !atomic.CompareAndSwapUint32((*uint32)(sl), 0, 1) {
for i := 0; i < backoff; i++ {
runtime.Gosched()
}
backoff <<= 1
}
}
func (sl *spinLock) Unlock() {
atomic.StoreUint32((*uint32)(sl), 0)
}
// NewSpinLock instantiates a spin-lock.
func NewSpinLock() sync.Locker {
return new(spinLock)
}
另外這里使用了指數(shù)退避,先等 1 個(gè)循環(huán)周期,通過runtime.Gosched()
告訴運(yùn)行時(shí)切換其他 goroutine 運(yùn)行。如果還是獲取不到鎖,就再等 2 個(gè)周期。如果還是不行,再等 4,8,16...以此類推。這可以防止短時(shí)間內(nèi)獲取不到鎖,導(dǎo)致 CPU 時(shí)間的浪費(fèi)。
總結(jié)
ants
源碼短小精悍,沒有引用其他任何第三方庫。各種細(xì)節(jié)處理,各種性能優(yōu)化的點(diǎn)都是值得我們細(xì)細(xì)品味的。強(qiáng)烈建議大家讀一讀源碼。閱讀優(yōu)秀的源碼,能極大地提高自身的編碼素養(yǎng)。
大家如果發(fā)現(xiàn)好玩、好用的 Go 語言庫,歡迎到 Go 每日一庫 GitHub 上提交 issue??
參考
- ants GitHub:github.com/panjf2000/ants
- Go 每日一庫 GitHub:https://github.com/darjun/go-daily-lib
我
歡迎關(guān)注我的微信公眾號(hào)【GoUpUp】,共同學(xué)習(xí),一起進(jìn)步~