[轉]Golang線程池實現百萬級高併發

 

轉,原文: https://lk668.github.io/2021/03/22/2021-03-22-Golang%E7%BA%BF%E7%A8%8B%E6%B1%A0%E5%AE%9E%E7%8E%B0%E7%99%BE%E4%B8%87%E7%BA%A7%E9%AB%98%E5%B9%B6%E5%8F%91/

--------------

Golang線程池實現百萬級高併發

本文基於Golang實現線程池,從而可以達到百萬級別的高併發請求。本文實現的代碼在https://github.com/lk668/threadpool可見。

1. Golang併發簡介

Golang原生的goroutine可以很輕鬆實現併發編程。Go語言的併發是基於用戶態的併發,這種併發方式就變得非常輕量,能夠輕鬆運行幾萬併發邏輯。Go 的併發屬於 CSP 併發模型的一種實現,CSP 併發模型的核心概念是:不要通過共享內存來通信,而應該通過通信來共享內存。

2. 併發方案演進

2.1 直接使用goroutine

直接使用goroutine啓動一個新的線程來進行任務的執行

1
go handler(request)

2.2 緩衝隊列

利用channel實現一個緩衝隊列,每次請求先放入緩衝隊列,然後從緩衝隊列讀取數據,一次執行

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
type Job interface{
Run()
}

// 長度爲1000的緩衝隊列
jobQueue := make(chan Job, 1000)

// 啓動一個協程來讀取緩衝隊列的數據
go func(){
for {
select {
case job := <- jobQueue:
job.Run()
}
}
}()

// 請求發送
job := Job{}
jobQueue <- job

該方案在請求量低於緩衝隊列長度時,可以應對併發請求。但是當併發請求量大於緩衝隊列長度時,channel會出現阻塞情況。

2.3 線程池實現百萬級高併發

更好的實現方案是利用job隊列+線程池來實現,具體如下所示:

有個全局JobQueue,用來存儲需要執行的Job,有個WorkerPool的線程池,用來存儲空閒的Worker。當JobQueue中有Job時,從JobQueue獲取Job對象,從WorkerPool獲取空閒Worker,將Job對象發送給Worker,進行執行。每個Worker都是一個獨立的Goroutine。從而真正意義上實現高併發。

代碼實現主要分爲三部分

2.3.1 Job定義

Job是一個interface,其下有個函數RunTask,用戶定製化的任務,需要實現RunTask函數。JobChan是一個Job channel結構。Job是高併發需要執行的任務

1
2
3
4
5
type Job interface {
RunTask(request interface{})
}

type JobChan chan Job

2.3.2 Worker定義

Worker就是高併發裏面的一個線程,啓動的時候是一個Goroutine。Worker結構一需要一個JobChan,用來接收從全局JobQueue裏面獲取的Job對象。有個Quit的channel,用來接收退出信號。需要一個Start函數,將自己註冊到WorkerPool,然後監聽Job,有Job傳入時,處理Job的RunTask,處理完成之後,重新將自己添加回WorkerPool。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
// Worker結構體
// WorkerPool隨機選取一個Worker,將Job發送給Worker去執行
type Worker struct {
// 不需要帶緩衝的任務隊列
JobQueue JobChan
//退出標誌
Quit chan bool
}

// 創建一個新的Worker對象
func NewWorker() Worker {
return Worker{
make(JobChan),
make(chan bool),
}
}

// 啓動一個Worker,來監聽Job事件
// 執行完任務,需要將自己重新發送到WorkerPool
func (w Worker) Start(workerPool *WorkerPool) {
// 需要啓動一個新的協程,從而不會阻塞
go func() {
for {
// 將worker註冊到線程池
workerPool.WorkerQueue <- &w
select {
case job := <-w.JobQueue:
job.RunTask(nil)
// 終止當前worker
case <-w.Quit:
return
}
}
}()
}

2.3.3 WorkerPool定義

WorkerPool是一個線程池,用來存儲Worker。所以需要一個Size變量,用來表示這個Pool存儲的Worker的個數。需要一個JobQueue,用來充當全局JobQueue的作用,所有的job先存儲到該全局JobQueue中。有個WorkerQueue是個channel,用來存儲空閒的Worker,有Job來的時候,從WorkerQueue裏面取出一個Worker,執行相應的任務,執行完成以後,重新將Worker放回WorkerQueue。

WorkerPool需要一個啓動函數,一個是來啓動Size數量的Worker線程,一個是需要啓動一個新的線程,來接收Job。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
// 線程池
type WorkerPool struct {
// 線程池大小
Size int
// 不帶緩衝的任務隊列,任務到達後,從workerQueue隨機選取一個Worker來執行Job
JobQueue JobChan
WorkerQueue chan *Worker
}

func NewWorkerPool(poolSize, jobQueueLen int) *WorkerPool {
return &WorkerPool{
poolSize,
make(JobChan, jobQueueLen),
make(chan *Worker, poolSize),
}
}

func (wp *WorkerPool) Start() {

// 將所有worker啓動
for i := 0; i < wp.Size; i++ {
worker := NewWorker()
worker.Start(wp)
}

// 監聽JobQueue,如果接收到請求,隨機取一個Worker,然後將Job發送給該Worker的JobQueue
// 需要啓動一個新的協程,來保證不阻塞
go func() {
for {
select {
case job := <-wp.JobQueue:
worker := <-wp.WorkerQueue
worker.JobQueue <- job
}
}
}()

}

2.3.4 代碼調用舉例

接下來,舉例分析如何使用該線程池。首先需要定義你要執行的任務,實現RunTask函數。然後初始化一個WorkerPool,將模擬百萬請求的數據發送給全局JobQueue。交給線程池進行任務處理。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
//需要執行任務的結構體
type Task struct {
Number int
}

// 實現Job這個interface的RunTask函數
func (t Task) RunTask(request interface{}) {
fmt.Println("This is task: ", t.Number)
//設置個等待時間
time.Sleep(1 * time.Second)
}

func main() {

// 設置線程池的大小
poolNum := 100 * 100 * 20
jobQueueNum := 100
workerPool := threadpool.NewWorkerPool(poolNum, jobQueueNum)
workerPool.Start()

// 模擬百萬請求
dataNum := 100 * 100 * 100

go func() {
for i := 0; i < dataNum; i++ {
task := Task{Number: i}
workerPool.JobQueue <- task
}
}()

// 阻塞主線程
for {
fmt.Println("runtime.NumGoroutine() :", runtime.NumGoroutine())
time.Sleep(2 * time.Second)
}
}

參考

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