Go 的線程池和協程池,看這一篇你就懂了
Golang 線程池與協程池是併發編程中的重要概念,它們可以幫助我們更高效地管理併發任務,提高程序的性能和資源利用率。下面我將詳細解釋這兩個概念,包括它們的實現方式、使用場景以及原理。
線程池(Thread Pool)
概念: 線程池是一種併發設計模式,用於管理線程的創建、銷燬和複用。線程池維護着多個線程,這些線程可以被用來執行任務,任務完成後線程並不立即銷燬,而是返回線程池中等待下一個任務。這樣可以減少線程創建和銷燬的開銷,提高系統性能。
線程池原理: 線程池的原理是通過維護一個線程隊列和任務隊列,線程從線程隊列中獲取任務並執行。當任務數量大於線程數量時,任務會等待;當線程數量大於任務數量時,線程會等待。這樣可以避免頻繁創建和銷燬線程的開銷。
實現方式: 在 Golang 中,由於其原生的併發模型是基於協程(Goroutine)的,因此 Golang 並沒有直接提供線程池的概念。但是,我們可以通過創建多個協程並複用它們來實現類似線程池的功能。
具體實現可以通過以下步驟:
-
創建一個固定大小的協程池。
-
爲每個協程分配任務。
-
協程完成任務後,返回並等待下一個任務。
package main
import (
"fmt"
"sync"
"time"
)
// Worker 是一個協程,它實現了執行任務的接口
type Worker struct {
wg *sync.WaitGroup
sem chan struct{}
}
// NewWorker 創建一個新的 Worker
func NewWorker(wg *sync.WaitGroup, sem chan struct{}) *Worker {
return &Worker{
wg: wg,
sem: sem,
}
}
// Task 是 Worker 執行的任務
func (w *Worker) Task() {
w.sem <- struct{}{}
// 這裏執行具體的任務
fmt.Println("Task is running...")
<-w.sem
}
func main() {
var wg sync.WaitGroup
sem := make(chan struct{}, 5) // 限制同時運行的協程數量
// 創建線程池
for i := 0; i < 5; i++ {
wg.Add(1)
go func(wg *sync.WaitGroup, sem chan struct{}) {
defer wg.Done()
worker := NewWorker(wg, sem)
for {
select {
case <-sem:
worker.Task()
}
}
}(&wg, sem)
}
// 發佈任務
for i := 0; i < 20; i++ {
wg.Add(1)
go func(wg *sync.WaitGroup) {
defer wg.Done()
// 這裏模擬獲取協程資源
<-sem
fmt.Println("Get a worker, starting task", time.Now())
// 模擬任務執行時間
time.Sleep(1 * time.Second)
fmt.Println("Task finished", time.Now())
}
}
// 等待所有任務完成
wg.Wait()
}
測試流程
運行上述代碼。
觀察控制檯輸出,檢查是否每個任務都按照預期執行。
注意協程池的大小限制爲 5,這意味着同時只能有 5 個任務在運行。
驗證任務是否按照順序被分配和執行。
預期結果
控制檯輸出應該顯示任務是按照發布順序開始執行的。
由於協程池的大小限制爲 5,所以在任何時刻,最多隻有 5 個任務會同時運行。
任務執行的時間是 2 秒,因此每個任務的輸出間隔大約是 2 秒。
任務完成後,控制檯會顯示 “Task finished” 消息,並顯示完成時間。
協程池(Goroutine Pool)
概念: 協程池與線程池類似,但它是基於 Golang 的協程(Goroutine)的。協程是 Golang 中輕量級的線程,它們不是由操作系統內核管理,而是由 Go 運行時管理。協程池可以複用協程,減少協程的創建和銷燬開銷。
實現方式: 協程池的實現與線程池類似,只是在 Golang 中我們創建的是協程而不是線程。以下是一個簡單的協程池實現:
// 與上面的線程池實現類似,只是這裏創建的是協程而不是線程
協程池原理: 協程池的原理與線程池相似,但是由於協程的輕量級特性,協程池在 Golang 中更爲常見和高效。協程池通過維護一個協程隊列和任務隊列來管理協程的執行。由於 Golang 中的協程(Goroutine)本質上就是線程池的一種實現,因此我們通常不需要顯式地創建一個 “協程池”。在上面的線程池案例中,我們已經展示瞭如何通過限制協程的數量來模擬協程池的行爲。在 Golang 中,我們通常直接使用協程來處理併發任務。
實戰案例詳解
假設我們需要實現一個簡單的 HTTP 服務器,它需要處理大量的併發請求。我們可以使用協程池來管理這些協程,以便高效地處理請求。
// HTTP 服務器的請求處理函數
func handleRequest(w http.ResponseWriter, r *http.Request) {
// 處理請求的邏輯
fmt.Fprintf(w, "Handling request from %s", r.RemoteAddr)
}
func main() {
// 創建一個協程池
pool := make(chan struct{}, 100) // 限制同時運行的協程數量
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
// 從協程池中獲取資源
pool <- struct{}{}
go func() {
defer func() {
recover() // 處理可能的 panic
pool <- struct{}{} // 釋放資源
}()
handleRequest(w, r)
}()
})
// 啓動 HTTP 服務器
log.Fatal(http.ListenAndServe(":8080", nil))
}
測試流程
運行上述代碼以啓動 HTTP 服務器。
使用瀏覽器或 HTTP 客戶端(如 curl)向服務器發送多個併發請求。
觀察服務器的響應時間和處理請求的順序。
預期結果
服務器應該能夠同時處理多個併發請求。
線程池與協程池的選擇
在選擇線程池還是協程池時,需要考慮以下因素:
-
性能: 協程比線程更輕量級,上下文切換的開銷更小,因此在 Golang 中協程池通常是更好的選擇。
-
資源消耗: 線程是操作系統內核管理的,消耗資源更多;協程由 Go 運行時管理,資源消耗較少。
-
適用場景: 如果你的應用程序主要運行在 Golang 環境下,那麼協程池是更好的選擇。如果你需要與底層操作系統線程交互,那麼線程池可能更適合。
總的來說,在 Golang 中,協程池由於其輕量級和高效性,通常是首選的併發模型。然而,根據具體的應用場景和需求,線程池在某些情況下也可能有用。
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mp.weixin.qq.com/s/z1YD6R8slDTa8cIUy3b5TA