Go 的線程池和協程池,看這一篇你就懂了

Golang 線程池與協程池是併發編程中的重要概念,它們可以幫助我們更高效地管理併發任務,提高程序的性能和資源利用率。下面我將詳細解釋這兩個概念,包括它們的實現方式、使用場景以及原理。

線程池(Thread Pool)

概念: 線程池是一種併發設計模式,用於管理線程的創建、銷燬和複用。線程池維護着多個線程,這些線程可以被用來執行任務,任務完成後線程並不立即銷燬,而是返回線程池中等待下一個任務。這樣可以減少線程創建和銷燬的開銷,提高系統性能。

線程池原理: 線程池的原理是通過維護一個線程隊列和任務隊列,線程從線程隊列中獲取任務並執行。當任務數量大於線程數量時,任務會等待;當線程數量大於任務數量時,線程會等待。這樣可以避免頻繁創建和銷燬線程的開銷。

實現方式: 在 Golang 中,由於其原生的併發模型是基於協程(Goroutine)的,因此 Golang 並沒有直接提供線程池的概念。但是,我們可以通過創建多個協程並複用它們來實現類似線程池的功能。

具體實現可以通過以下步驟:

  1. 創建一個固定大小的協程池。

  2. 爲每個協程分配任務。

  3. 協程完成任務後,返回並等待下一個任務。

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 中,協程池由於其輕量級和高效性,通常是首選的併發模型。然而,根據具體的應用場景和需求,線程池在某些情況下也可能有用。

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/z1YD6R8slDTa8cIUy3b5TA