原文:FastHTTP源碼分析——「百花齊放」的協程池git
閱讀本編文章須要go語言基礎和對資源池有一些瞭解。github
go 版本爲1.11,FastHTTP爲2018-11-23的最新master版本數組
在開始前咱們先來簡單定義一下協程池:可以達到協程資源複用
。在這個定義下協程池的實現能夠說是「百花齊放」了,找一下熱門的go語言開源項目都會有協程池的不一樣實現方式。 有基於鏈表實現的Tidb,有基於環形隊列實現的Jaeger,有基於數組棧實現的FastHTTP等,種類繁多任君選擇。這麼多的協程池實現能夠概括成二種:app
這2種實現中,我的比較喜歡第二種按需建立,FastHTTP也是使用第二種方式,因此咱們來看看它是如何實現的。源碼分析
在介紹FastHTTP協程池以前先作一下簡單的介紹。workerChan和協程一一對應,相同的生命週期,能夠把workerChan當作是協程的門牌,使用憑證,引路子等。 整個協程池的實現主要由workerPool和workerChan組成。性能
http.Server
net/http/server.go #2805 func (srv *Server) Serve(l net.Listener) error { ...... for { rw, e := l.Accept() ...... //FastHTTP在這步使用協程池 go c.serve(ctx) } }
fasthttp.ListenAndServe
github.com/valyala/fasthttp/server.go 1489 func (s *Server) Serve(ln net.Listener) error { ...... for { if c, err = acceptConn(s, ln, &lastPerIPErrorTime); err != nil { ...... } //對應go原生的 go c.serve(ctx) if !wp.Serve(c) { ...... } ...... } }
在go原生的
http.Server
包中,當接收到新請求就會啓動一個協程處理,而FastHTTP則使用協程池處理。
github.com/valyala/fasthttp/workerpool.go #156 func (wp *workerPool) getCh() *workerChan { var ch *workerChan createWorker := false wp.lock.Lock() ready := wp.ready n := len(ready) - 1 if n < 0 { if wp.workersCount < wp.MaxWorkersCount { createWorker = true wp.workersCount++ } } else { //從尾部獲取Ch ch = ready[n] ready[n] = nil wp.ready = ready[:n] } wp.lock.Unlock() if ch == nil { //若是協程數超過上限,直接拋棄當前請求 if !createWorker { return nil } vch := wp.workerChanPool.Get() if vch == nil { vch = &workerChan{ ch: make(chan chan struct{}, workerChanCap), } } ch = vch.(*workerChan) //ch和協程綁定 go func() { wp.workerFunc(ch) wp.workerChanPool.Put(vch) }() } return ch }
在go語言中不一樣協程之間的通信使用channel
,在協程池中也不例外,FastHTTP建立了一個協程,就會和一個workerChan
綁定,使用方根據這個workerChan
就可使用協程池裏的資源。從上面的代碼能夠看出,使用協程池的資源,都是先從Slice的尾部彈出workerChan
,在把workerChan
交給使用放,若是Slice沒有workerChan
就會建立。
github.com/valyala/fasthttp/workerpool.go #194 func (wp *workerPool) release(ch *workerChan) bool { //用戶清理 ch.lastUseTime = time.Now() wp.lock.Lock() if wp.mustStop { wp.lock.Unlock() return false } //往尾部追加 wp.ready = append(wp.ready, ch) wp.lock.Unlock() return true }
當協程完成工做後,就會把
workerChan
放回Slice尾部,以待其餘請求使用。
workerChan
github.com/valyala/fasthttp/workerpool.go #98 func (wp *workerPool) clean(scratch *[]*workerChan) { ...... currentTime := time.Now() wp.lock.Lock() ready := wp.ready n := len(ready) i := 0 for i < n && currentTime.Sub(ready[i].lastUseTime) > maxIdleWorkerDuration { i++ } *scratch = append((*scratch)[:0], ready[:i]...) if i > 0 { m := copy(ready, ready[i:]) for i = m; i < n; i++ { ready[i] = nil } wp.ready = ready[:m] } wp.lock.Unlock() ...... tmp := *scratch for i, ch := range tmp { //讓協程中止工做 ch.ch <- nil tmp[i] = nil } }
按期清理是爲了不在常態下空閒的協程過多,加劇了調度層的負擔。使用按需建立協程池的方式存在這樣一個問題,高峯期的時候建立了不少協程,高峯期事後不少協程處於空閒狀態,這就形成了沒必要要的開銷。因此須要一種過時機制。在這裏數組棧(FILO)的優勢也體現出來了,由於棧的特色不活躍的workerChan
都放在了數組的頭部,因此只須要從數組頭部開始輪詢,一直到找到未過時的workerChan
,再把這部分清理掉,就達到清理的效果,而且不須要輪詢整個數組。
花了點時間對FastHTTP的協程池進行了壓測 代碼。
apple:gopool apple$ go test -bench=. -test.benchmem goos: darwin goarch: amd64 pkg: study_go/gopool BenchmarkNotPool-4 10 4937881320 ns/op 107818560 B/op 401680 allocs/op BenchmarkFastHttpPool-4 10 380807481 ns/op 13444607 B/op 169946 allocs/op BenchmarkAntsPoll-4 10 429482715 ns/op 20756724 B/op 302093 allocs/op PASS ok study_go/gopool 72.891s
從上面的對比來看使用協程池的收益還很多。
FastHTTP協程池的實現方式是我所瞭解的幾種實現中,性能是比較突出的,固然其餘協程池的實現方式也頗有學習參考價值,在這個過程當中複習了鏈表,數組棧,環形隊列的使用場景。收穫頗多。