Golang學習篇——協程池

目錄

1. 爲什麼需要協程池?

2. 簡單的協程池

3. go-playground/pool

4. ants(推薦)


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"

GitHub源碼參考

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與一個需要大批量重複執行的函數鎖綁定,避免了調用方不停的創建,更加節省內存。

ants源碼參考

 

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倍。

源碼參考:

ants源碼

go-playground/pool

fasthttp 

參考鏈接

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章