目錄
1. 爲什麼需要協程池?
雖然go語言自帶“高併發”的標籤,其併發編程就是由groutine實現的,因其消耗資源低(大約2KB左右,線程通常2M左右),性能高效,開發成本低的特性而被廣泛應用到各種場景,例如服務端開發中使用的HTTP服務,在golang net/http包中,每一個被監聽到的tcp鏈接都是由一個groutine去完成處理其上下文的,由此使得其擁有極其優秀的併發量吞吐量。
但是,如果無休止的開闢Goroutine依然會出現高頻率的調度Groutine,那麼依然會浪費很多上下文切換的資源,導致做無用功。所以設計一個Goroutine池限制Goroutine的開闢個數在大型併發場景還是必要的。
2. 簡單的協程池
package main
import (
"fmt"
"time"
)
/* 有關Task任務相關定義及操作 */
//定義任務Task類型,每一個任務Task都可以抽象成一個函數
type Task struct {
f func() error //一個無參的函數類型
}
//通過NewTask來創建一個Task
func NewTask(f func() error) *Task {
t := Task{
f: f,
}
return &t
}
//執行Task任務的方法
func (t *Task) Execute() {
t.f() //調用任務所綁定的函數
}
/* 有關協程池的定義及操作 */
//定義池類型
type Pool struct {
EntryChannel chan *Task //對外接收Task的入口
worker_num int //協程池最大worker數量,限定Goroutine的個數
JobsChannel chan *Task //協程池內部的任務就緒隊列
}
//創建一個協程池
func NewPool(cap int) *Pool {
p := Pool{
EntryChannel: make(chan *Task),
worker_num: cap,
JobsChannel: make(chan *Task),
}
return &p
}
//協程池創建一個worker並且開始工作
func (p *Pool) worker(work_ID int) {
//worker不斷的從JobsChannel內部任務隊列中拿任務
for task := range p.JobsChannel {
//如果拿到任務,則執行task任務
task.Execute()
fmt.Println("worker ID ", work_ID, " 執行完畢任務")
}
}
//讓協程池Pool開始工作
func (p *Pool) Run() {
//1,首先根據協程池的worker數量限定,開啓固定數量的Worker,
// 每一個Worker用一個Goroutine承載
for i := 0; i < p.worker_num; i++ {
fmt.Println("開啓固定數量的Worker:", i)
go p.worker(i)
}
//2, 從EntryChannel協程池入口取外界傳遞過來的任務
// 並且將任務送進JobsChannel中
for task := range p.EntryChannel {
p.JobsChannel <- task
}
//3, 執行完畢需要關閉JobsChannel
close(p.JobsChannel)
fmt.Println("執行完畢需要關閉JobsChannel")
//4, 執行完畢需要關閉EntryChannel
close(p.EntryChannel)
fmt.Println("執行完畢需要關閉EntryChannel")
}
//主函數
func main() {
//創建一個Task
t := NewTask(func() error {
fmt.Println("創建一個Task:", time.Now().Format("2006-01-02 15:04:05"))
return nil
})
//創建一個協程池,最大開啓3個協程worker
p := NewPool(3)
//開一個協程 不斷的向 Pool 輸送打印一條時間的task任務
go func() {
for {
p.EntryChannel <- t
}
}()
//啓動協程池p
p.Run()
}
3. go-playground/pool
上面的協程池雖然簡單,但是對於每一個併發任務的狀態,pool的狀態缺少控制,我們可以看看go-playground/pool的源碼實現,“源碼面前,如同裸奔”。先從每一個需要執行的任務入手,該庫中對併發單元做了如下的結構體,可以看到除工作單元的值,錯誤,執行函數等,還用了三個分別表示,取消,取消中,寫 的三個併發安全的原子操作值來標識其運行狀態。
依賴包下載: go get "gopkg.in/go-playground/pool.v3"
package main
import (
"fmt"
"gopkg.in/go-playground/pool.v3"
"time"
)
func SendMail(int int) pool.WorkFunc {
fn := func(wu pool.WorkUnit) (interface{}, error) {
// sleep 1s 模擬發郵件過程
time.Sleep(time.Second * 1)
// 模擬異常任務需要取消
if int == 17 {
wu.Cancel()
}
if wu.IsCancelled() {
return false, nil
}
fmt.Println("send to", int)
return true, nil
}
return fn
}
func main() {
// 初始化groutine數量爲20的pool
p := pool.NewLimited(20)
defer p.Close()
batch := p.Batch()
// 設置一個批量任務的過期超時時間
t := time.After(10 * time.Second)
go func() {
for i := 0; i < 100; i++ {
batch.Queue(SendMail(i)) // 往批量任務中添加workFunc任務
}
// 通知批量任務不再接受新的workFunc, 如果添加完workFunc不執行改方法的話將導致取結果集時done channel一直阻塞
batch.QueueComplete()
}()
// // 獲取批量任務結果集, 因爲 batch.Results 中要close results channel 所以不能將其放在LOOP中執行
r := batch.Results()
LOOP:
for {
select {
case <-t:
// 超時通知
fmt.Println("超時通知")
break LOOP
case email, ok := <-r:
// 讀取結果集
if ok {
if err := email.Error(); err != nil {
fmt.Println("讀取結果集錯誤,error info:", err.Error())
}
fmt.Println("錯誤結果集:", email.Value())
} else {
fmt.Println("finish")
break LOOP
}
}
}
}
go-playground/pool相比簡單的協程池, 對pool, worker的狀態有了很好的管理。但是在第一個實現的簡單groutine池和go-playground/pool中,都是先啓動預定好的groutine來完成任務執行,在併發量遠小於任務量的情況下確實能夠做到groutine的複用,如果任務量不多則會導致任務分配到每個groutine不均勻,甚至可能出現啓動的groutine根本不會執行任務從而導致浪費,而且對於協程池也沒有動態的擴容和縮小。接下來了解下ants的設計和實現。
4. ants(推薦)
ants是一個受fasthttp啓發的高性能協程池,fasthttp號稱是比go原生的net/http快10倍,其原因之一就是採用了各種池化技術, ants相比之前兩種協程池,其模型更像是之前接觸到的數據庫連接池,需要從空餘的worker中取出一個來執行任務, 當無可用空餘worker的時候再去創建,而當pool的容量達到上線之後,剩餘的任務阻塞等待當前進行中的worker執行完畢將worker放回pool, 直至pool中有空閒worker。 ants在內存的管理上做得很好,除了定期清除過期worker(一定時間內沒有分配到任務的worker),ants還實現了一種適用於大批量相同任務的pool, 這種pool與一個需要大批量重複執行的函數鎖綁定,避免了調用方不停的創建,更加節省內存。
package main
import (
"fmt"
"github.com/panjf2000/ants"
"sync"
"time"
)
//任務
func sendMail(i int, wg *sync.WaitGroup) func() {
var cnt int
return func() {
for {
time.Sleep(time.Second * 2)
fmt.Println("send mail to ", i)
cnt++
if cnt > 5 && i == 1 {
fmt.Println("退出協程ID:", i)
break
}
}
wg.Done()
}
}
func main() {
wg := sync.WaitGroup{}
//申請一個協程池對象
pool, _ := ants.NewPool(2)
//關閉協程池
defer pool.Release()
// 向pool提交任務
for i := 1; i <= 5; i++ {
pool.Submit(sendMail(i, &wg))
wg.Add(1)
}
wg.Wait()
}
源碼中提到, ants的吞吐量能夠比原生groutine高出N倍,內存節省10到20倍。
源碼參考: