golang 開發: goroutine 在項目中的使用姿勢

很多初級的 Gopher 在學習了 goroutine 之後,在項目中其實使用率不高,尤其一些跨語言過來的人,對併發編程理解不深入,可能很多人只知道 go func(), 或者掌控不夠,謹慎一些,儘量少使用或者不使用,用的話就是 go func(),主要列一下我們這邊的主要使用方法。

goroutine 在項目中的使用方法

看一下樣例代碼,實際上,我們生產環境中就是這麼使用的。

package logic

import (
    "context"
    "fmt"
    "sync"
    "time"
)

type UserData struct {
    Age     int
    Name    string
    Postion string
}

type ServerLogic struct {
    ctx    context.Context
    cancel func()
    waiter sync.WaitGroup
    ch     chan UserData
}

func NewServerLogic(logCtx *context.Context, worker int, queue int) *ServerLogic {
    logic := &ServerLogic{}
    logic.InitWorker(worker, queue)
    return logic
}

func (this *ServerLogic) InitWorker(workers int, queue int) {
    this.ch = make(chan UserData, queue)
    this.ctx, this.cancel = context.WithCancel(context.Background())
    this.waiter.Add(workers)
    for i := 0; i < workers; i++ {
        go this.Proc()
    }
}

func (this *ServerLogic) Proc() {
    defer this.waiter.Done()
    for {
        select {
        case t := <-this.ch:
            this.Dothing(t)
        case <-this.ctx.Done():
            return
        }
    }
}

func (this *ServerLogic) Dothing(data UserData) error {
    //do code
    time.Sleep(time.Second*30)
    return nil
}

func (this *ServerLogic) Close() {
    this.cancel()
    this.waiter.Wait()
}

func (this *ServerLogic) PutData(user UserData) error {
    select {
    case this.ch<-user:
        return nil
    default:
        return fmt.Errorf("queue overflow")
    }
}

如果有人想直接使用的話,只需要把 UserData struct 換成自己的請求數據,把 Dothing 裏面的代碼換成讓 goroutine 多任務執行的代碼就可以在自己的項目中使用了。

PutData 有請求數據就放入 channel,每個 goroutine 不停的循環從 channel 裏面取數據,取到數據之後就執行相應的邏輯流程,可以看到整體的調度都是 channel 來控制的,通過 channel 的通信來傳遞數據。

不要通過共享內存來通信, 要通過通信來共享內存

看看大概的代碼分析

  1. 1. InitWorker 的時候會創建 queue 個 channl,再創建 workers 個 goroutine,執行 go Proc()

    1. Proc 方法,裏面有 for 的無限循環,不停從步驟 1 裏面創建的 channl 裏面獲取 UserData 數據,一旦獲取數據成功,就會帶着 UserData 數據去執行 Dothing 方法。需要注意的是,這是 workers 個 goroutine 都在執行 Proc
  2. 3. Dothing 方法,就是讓某一個 goroutine 拿到 UserData 數據去處理數據,執行邏輯

    1. Close 方法,給所有的 goroutine 發送關閉的信號,channl 裏面不在有數據寫入,waiter.Wait() 等待現有的 channel 裏面數據被消費完,goroutine 就執行完畢退出。
    1. PutData 方法,就是把請求的數據交給 goroutine 去執行。具體的做法,是把數據 塞到 channl 隊列裏面,如果 queue 個 channl 隊列已滿,就拋出溢出錯誤。

當然了 PutData 也可以等待 channl 隊列裏面的數據被 Proc 拿出,然後空出位置再塞數據到 channl 隊列。

func (this *ServerLogic) PutData(user UserData) error {
    timer := time.NewTimer(3*time.Second)
    select {
    case this.ch<-user:
        return nil
    case <-timer.C:
        return fmt.Errorf("put timeout")
    }
}

加一個超時器,總不能等到天荒地老把,如果超過三秒,仍然沒有空出 channl 位置,現有的隊列還沒有消費完,就拋出塞數據超時的錯誤.

看一下樣例的使用的代碼

package main

import (
    context2 "context"
    "fmt"
    "test/logic"
)

func main() {
    context := context2.Background()
    server := logic.NewServerLogic(&context, 1, 2)
    rt1 := server.PutData(logic.UserData{
        Age:     11,
        Name:    "test1",
        Postion: "golang",
    })
    fmt.Println(rt1)
    rt2 := server.PutData(logic.UserData{
        Age:     12,
        Name:    "test2",
        Postion: "golang",
    })
    fmt.Println(rt2)
    rt3 := server.PutData(logic.UserData{
        Age:     13,
        Name:    "test3",
        Postion: "golang",
    })
    fmt.Println(rt3)

    server.Close()
    fmt.Println("end")
}

等待了大概三十多秒之後的結果, 打印結果其實跟預想的是一樣的。

<nil>
<nil>
queue overflow
end

NewServerLogic(&context, 1, 2) 代碼中,我們要求創建了 1 個 goroutine,大小爲 2 的 channl 隊列。所以第一個 PutData 和第二個 PutData 是塞數據成功的。等到第三次 PutData 的時候,因爲我們 channl 隊列的大小是 2,已經被佔滿了,所以第三次就會提示溢出錯誤。

使用 goroutine 另一種方法

我看項目中還有一些其他人的使用方法,區別只是退出的時候沒有使用 context 的 cancel 方法,而是使用了 channel 去通知退出 goroutine,內部的原理其實是一樣的。看一下下面的代碼。

只有關閉這裏是不一樣的,其他的基本一致。執行退出的時候在 Close() 方法中,close(this.quit) 會給 quit channel 寫入數據,Proc() 方法會循環從 channel 和 quit 裏面取數據,一旦從 this.quit 裏面取出了數據,說明系統讓關閉 goroutine,然後 Proc 方法就終止。

go func() 行不行

有人說,扯這麼多,爲啥 go func() 不行,我在項目裏面使用 go func() 運行的好好,而且 golang 的 HTTP 庫裏也是使用的 go c.serve(ctx)。我的理解是主要看使用場景,如果你的服務對結果要求不是 100% 的成功,對併發的要求很高,那就可以使用 go func(),go c.serve(ctx) 也是類似,TCP 本身就是不可靠的連接,HTTP 也允許有極少量的失敗狀態。

如果你的服務裏面只是想讓多個 goroutine 處理你的數據,不希望這個 goroutine 太多影響你的主幹服務,或者你爲了提高數據處理效率,想讓多個 goroutine 去請求第三方的服務,這樣的話,就應該創建若干個 goroutine 去併發處理你的任務,也不建議直接 go func(),goroutine 數量不可控,會影響其他的主幹服務或者佔用服務器資源,如果請求第三方的服務,可能會因爲併發太高被限制,或者把第三方服務打掛。我們就遇到過這種情況。總之,使用場景很重要,不是一概而論的。

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