Go基礎系列:Go實現工作池的兩種方式

来源:https://www.cnblogs.com/f-ck-need-u/archive/2018/11/22/10004620.html
-Advertisement-
Play Games

worker pool簡介 worker pool其實就是線程池thread pool。對於go來說,直接使用的是goroutine而非線程,不過這裡仍然以線程來解釋線程池。 線上程池模型中, 有2個隊列一個池子:任務隊列、已完成任務隊列和線程池 。其中已完成任務隊列可能存在也可能不存在,依據實際需 ...


worker pool簡介

worker pool其實就是線程池thread pool。對於go來說,直接使用的是goroutine而非線程,不過這裡仍然以線程來解釋線程池。

線上程池模型中,有2個隊列一個池子:任務隊列、已完成任務隊列和線程池。其中已完成任務隊列可能存在也可能不存在,依據實際需求而定。

只要有任務進來,就會放進任務隊列中。只要線程執行完了一個任務,就將任務放進已完成任務隊列,有時候還會將任務的處理結果也放進已完成隊列中。

worker pool中包含了一堆的線程(worker,對go而言每個worker就是一個goroutine),這些線程嗷嗷待哺,等待著為它們分配任務,或者自己去任務隊列中取任務。取得任務後更新任務隊列,然後執行任務,並將執行完成的任務放進已完成隊列。

下圖來自wiki:

在Go中有兩種方式可以實現工作池:傳統的互斥鎖、channel。

傳統互斥鎖機制的工作池

假設Go中的任務的定義形式為:

type Task struct {
    ...
}

每次有任務進來時,都將任務放在任務隊列中。

使用傳統的互斥鎖方式實現,任務隊列的定義結構大概如下:

type Queue struct{
    M     sync.Mutex
    Tasks []Task
}

然後在執行任務的函數中加上Lock()和Unlock()。例如:

func Worker(queue *Queue) {
    for {
        // Lock()和Unlock()之間的是critical section
        queue.M.Lock()
        // 取出任務
        task := queue.Tasks[0]
        // 更新任務隊列
        queue.Tasks = queue.Tasks[1:]
        queue.M.Unlock()
        // 在此goroutine中執行任務
        process(task)
    }
}

假如線上程池中激活了100個goroutine來執行Worker()。Lock()和Unlock()保證了在同一時間點只能有一個goroutine取得任務並隨之更新任務列表,取任務和更新任務隊列都是critical section中的代碼,它們是具有原子性。然後這個goroutine可以執行自己取得的任務。於此同時,其它goroutine可以爭奪互斥鎖,只要爭搶到互斥鎖,就可以取得任務並更新任務列表。當某個goroutine執行完process(task),它將因為for迴圈再次參與互斥鎖的爭搶。

上面只是給出了一點主要的代碼段,要實現完整的線程池,還有很多額外的代碼。

通過互斥鎖,上面的一切操作都是線程安全的。但問題在於加鎖/解鎖的機制比較重量級,當worker(即goroutine)的數量足夠多,鎖機制的實現將出現瓶頸。

通過buffered channel實現工作池

在Go中,也能用buffered channel實現工作池。

示例代碼很長,所以這裡先拆分解釋每一部分,最後給出完整的代碼段。

在下麵的示例中,每個worker的工作都是計算每個數值的位數相加之和。例如給定一個數值234,worker則計算2+3+4=9。這裡交給worker的數值是隨機生成的[0,999)範圍內的數值。

這個示例有幾個核心功能需要先解釋,也是通過channel實現線程池的一般功能:

  • 創建一個task buffered channel,並通過allocate()函數將生成的任務存放到task buffered channel中
  • 創建一個goroutine pool,每個goroutine監聽task buffered channel,並從中取出任務
  • goroutine執行任務後,將結果寫入到result buffered channel中
  • 從result buffered channel中取出計算結果並輸出

首先,創建Task和Result兩個結構,並創建它們的通道:

type Task struct {
    ID      int
    randnum int
}

type Result struct {
    task    Task
    result  int
}

var tasks = make(chan Task, 10)
var results = make(chan Result, 10)

這裡,每個Task都有自己的ID,以及該任務將要被worker計算的隨機數。每個Result都包含了worker的計算結果result以及這個結果對應的task,這樣從Result中就可以取出任務信息以及計算結果。

另外,兩個通道都是buffered channel,容量都是10。每個worker都會監聽tasks通道,並取出其中的任務進行計算,然後將計算結果和任務自身放進results通道中。

然後是計算位數之和的函數process(),它將作為worker的工作任務之一。

func process(num int) int {
    sum := 0
    for num != 0 {
        digit := num % 10
        sum += digit
        num /= 10
    }
    time.Sleep(2 * time.Second)
    return sum
}

這個計算過程其實很簡單,但隨後還睡眠了2秒,用來假裝執行一個計算任務是需要一點時間的。

然後是worker(),它監聽tasks通道並取出任務進行計算,並將結果放進results通道。

func worker(wg *WaitGroup){
    defer wg.Done()
    for task := range tasks {
        result := Result{task, process(task.randnum)}
        results <- result
    }
}

上面的代碼很容易理解,只要tasks channel不關閉,就會一直監聽該channel。需要註意的是,該函數使用指針類型的*WaitGroup作為參數,不能直接使用值類型的WaitGroup作為參數,這樣會使得每個worker都有一個自己的WaitGroup。

然後是創建工作池的函數createWorkerPool(),它有一個數值參數,表示要創建多少個worker。

func createWorkerPool(numOfWorkers int) {
    var wg sync.WaitGroup
    for i := 0; i < numOfWorkers; i++ {
        wg.Add(1)
        go worker(&wg)
    }
    wg.Wait()
    close(results)
}

創建工作池時,首先創建一個WaitGroup的值wg,這個wg被工作池中的所有goroutine共用,每創建一個goroutine都wg.Add(1)。創建完所有的goroutine後等待所有的groutine都執行完它們的任務,只要有一個任務還沒有執行完,這個函數就會被Wait()阻塞。當所有任務都執行完成後,關閉results通道,因為沒有結果再需要向該通道寫了。

當然,這裡是否需要關閉results通道,是由稍後的range迭代這個通道決定的,不關閉這個通道會一直阻塞range,最終導致死鎖。

工作池部分已經完成了。現在需要使用allocate()函數分配任務:生成一大堆的隨機數,然後將Task放進tasks通道。該函數有一個代表創建任務數量的數值參數:

func allocate(numOfTasks int) {
    for i := 0; i < numOfTasks; i++ {
        randnum := rand.Intn(999)
        task := Task{i, randnum}
        tasks <- task
    }
    close(tasks)
}

註意,最後需要關閉tasks通道,因為所有任務都分配完之後,沒有任務再需要分配。當然,這裡之所以需要關閉tasks通道,是因為worker()中使用了range迭代tasks通道,如果不關閉這個通道,worker將在取完所有任務後一直阻塞,最終導致死鎖。

再接著的是取出results通道中的結果進行輸出,函數名為getResult():

func getResult(done chan bool) {
    for result := range results {
        fmt.Printf("Task id %d, randnum %d , sum %d\n", result.task.id, result.task.randnum, result.result)
    }
    done <- true
}

getResult()中使用了一個done參數,這個參數是一個信號通道,用來表示results中的所有結果都取出來並處理完成了,這個通道不一定要用bool類型,任何類型皆可,它不用來傳數據,僅用來返回可讀,所以上面直接close(done)的效果也一樣。通過下麵的main()函數,就能理解done信號通道的作用。

最後還差main()函數:

func main() {
    // 記錄起始終止時間,用來測試完成所有任務耗費時長
    startTime := time.Now()
    
    numOfWorkers := 20
    numOfTasks := 100
    // 創建任務到任務隊列中
    go allocate(numOfTasks)
    // 創建工作池
    go createWorkerPool(numOfWorkers)
    // 取得結果
    var done = make(chan bool)
    go getResult(done)

    // 如果results中還有數據,將阻塞在此
    // 直到發送了信號給done通道
    <- done
    endTime := time.Now()
    diff := endTime.Sub(startTime)
    fmt.Println("total time taken ", diff.Seconds(), "seconds")
}

上面分配了20個worker,這20個worker總共需要處理的任務數量為100。但註意,無論是tasks還是results通道,容量都是10,意味著任務隊列最長只能是10個任務。

下麵是完整的代碼段:

package main

import (
    "fmt"
    "math/rand"
    "sync"
    "time"
)

type Task struct {
    id      int
    randnum int
}
type Result struct {
    task   Task
    result int
}

var tasks = make(chan Task, 10)
var results = make(chan Result, 10)

func process(num int) int {
    sum := 0
    for num != 0 {
        digit := num % 10
        sum += digit
        num /= 10
    }
    time.Sleep(2 * time.Second)
    return sum
}
func worker(wg *sync.WaitGroup) {
    defer wg.Done()
    for task := range tasks {
        result := Result{task, process(task.randnum)}
        results <- result
    }
}
func createWorkerPool(numOfWorkers int) {
    var wg sync.WaitGroup
    for i := 0; i < numOfWorkers; i++ {
        wg.Add(1)
        go worker(&wg)
    }
    wg.Wait()
    close(results)
}
func allocate(numOfTasks int) {
    for i := 0; i < numOfTasks; i++ {
        randnum := rand.Intn(999)
        task := Task{i, randnum}
        tasks <- task
    }
    close(tasks)
}
func getResult(done chan bool) {
    for result := range results {
        fmt.Printf("Task id %d, randnum %d , sum %d\n", result.task.id, result.task.randnum, result.result)
    }
    done <- true
}
func main() {
    startTime := time.Now()
    numOfWorkers := 20
    numOfTasks := 100

    var done = make(chan bool)
    go getResult(done)
    go allocate(numOfTasks)
    go createWorkerPool(numOfWorkers)
    // 必須在allocate()和getResult()之後創建工作池
    <-done
    endTime := time.Now()
    diff := endTime.Sub(startTime)
    fmt.Println("total time taken ", diff.Seconds(), "seconds")
}

執行結果:

Task id 19, randnum 914 , sum 14
Task id 9, randnum 150 , sum 6
Task id 15, randnum 215 , sum 8
............
Task id 97, randnum 315 , sum 9
Task id 99, randnum 641 , sum 11
total time taken  10.0174705 seconds

總共花費10秒。

可以試著將任務數量、worker數量修改修改,看看它們的性能比例情況。例如,將worker數量設置為99,將需要4秒,將worker數量設置為10,將需要20秒。


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • FileManager 圖片管理插件是 TinyMCE下 免費且非官方的文件插件,可上傳/管理的類型包括:文件,圖片,視頻。 本教程主要展示大叔在為安裝開發插件過程中進行調整的修改記錄。 ...
  • 昨天是感恩節,上幼兒園的女兒在老師的叮囑下,晚上為我和老婆洗了腳(形式上的^_^),還給我們每人端了一杯水。看著孩子一天天的長大,懂事,感覺很開心。話說咱們程式員這麼辛苦是為了什麼?不就是為了老婆,孩子,熱炕頭,有一個溫暖幸福的家庭,再捎帶著用代碼改變一下世界嗎?想到這裡,頓時覺得學習,創作博客的勁... ...
  • 有人說,你應該關註時事、財經,甚至流行的電影、電視劇,才有可能趁著熱點寫出爆文;有人說,你別再寫“無聊”的技術文了,因為程式員的圈子真的很小,即便是像鴻祥那樣的招牌大牛,文章是那麼的乾貨,瀏覽量有多少?不到萬吧;有人說,你別妄想在寫作上面知識變現了,因為你寫的文章真的很不優秀,我都不愛看! 我想說, ...
  • Django中每一個模型model都對應於資料庫中的一張表,每個模型中的欄位都對應於資料庫表的列。方便的是,django可以自動生成這些create table, alter table, drop table的操作。其次Django為咱們也提供了後臺管理模塊(Django-Admin),主要功能是 ...
  • 原地址:https://www.cnblogs.com/hongten/p/hongten_java_sleep_wait.html ...
  • 1.網路通信協議 osi七層模型:按照分工不同把互聯網協議從邏輯上劃分了層級 socket層 2.理解socket: Socket是應用層與TCP/IP協議族通信的中間軟體抽象層,它是一組介面。在設計模式中,Socket其實就是一個門面模式,它把複雜的TCP/IP協議族隱藏在Socket介面後面,對 ...
  • 正常情況下,新激活的goroutine的結束過程是不可控制的,唯一可以保證終止goroutine的行為是main goroutine的終止。也就是說,我們並不知道哪個goroutine什麼時候結束。 但很多情況下,我們正需要知道goroutine是否完成。這需要藉助sync包的WaitGroup來實 ...
  • 1.C/S架構 C/S架構:Client與Server ,中文意思:客戶端與伺服器端架構,這種架構也是從用戶層面(也可是物理層面)來劃分的。這裡客戶端一般指需先安裝再執行的應用程式,對操作系統依賴性較大;服務端即是這類程式對應的伺服器。 B/S架構:browser/server,瀏覽器端與伺服器端架 ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...