Golang中context使用

董雷發表於2022-02-17
[TOC]

1. 為什麼需要context

  • 在併發程式中,由於超時、取消操作或者一些異常情況,往往需要進行搶佔操作或者中斷後續操作。

  • 舉個例子:在 Go http包的Server中,每一個請求在都有一個對應的 goroutine 去處理。請求處理函式通常會啟動額外的 goroutine 用來訪問後端服務,比如資料庫和RPC服務,用來處理一個請求的 goroutine 通常需要訪問一些與請求特定的資料,比如終端使用者的身份認證資訊、驗證相關的token、請求的截止時間。 當一個請求被取消或超時時,所有用來處理該請求的 goroutine 都應該迅速中斷退出,然後系統才能釋放這些 goroutine 佔用的資源。context深入理解可參考

  • context常用的使用場景:

    1) 一個請求對應多個goroutine之間的資料互動
    2) 超時控制
    3) 上下文控制

2. context包簡介

context.Context介面:

type Context interface {
    // 返回Context的超時時間(超時返回場景)
    Deadline() (deadline time.Time, ok bool)
    // 在Context超時或取消時(即結束了)返回一個關閉的channel
    // 即如果當前Context超時或取消時,Done方法會返回一個channel,然後其他地方就可以通過判斷Done方法是否有返回(channel),如果有則說明Context已結束
    // 故其可以作為廣播通知其他相關方本Context已結束,請做相關處理。
    Done() <-chan struct{}
    // 返回Context取消的原因
    Err() error
    // 返回Context相關資料
    Value(key interface{}) interface{}
}

繼承的Context,BackGound是所有Context的root,不能夠被cancel。context包提供了三種context,分別是是普通context,超時context以及帶值的context:

// 普通context,通常這樣呼叫: ctx, cancel := context.WithCancel(context.Background())
func WithCancel(parent Context) (ctx Context, cancel CancelFunc)
// 帶超時的context,超時之後會自動close物件的Done,與呼叫CancelFunc的效果一樣
// WithDeadline 明確地設定一個d指定的系統時鐘時間,如果超過就觸發超時
// WithTimeout 設定一個相對的超時時間,也就是deadline設為timeout加上當前的系統時間
// 因為兩者事實上都依賴於系統時鐘,所以可能存在微小的誤差,所以官方不推薦把超時間隔設定得太小
// 通常這樣呼叫:ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
func WithDeadline(parent Context, d time.Time) (Context, CancelFunc)
func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc)
// 帶有值的context,沒有CancelFunc,所以它只用於值的多goroutine傳遞和共享
// 通常這樣呼叫:ctx := context.WithValue(context.Background(), "key", myValue)
func WithValue(parent Context, key, val interface{}) Context

3. 場景舉例—等待組

package main
import (
    "fmt"
    "sync"
    "time"
)
//資料接收服務主協程同子協程同步變數
var wg sync.WaitGroup
func run(i int) {
    fmt.Println("start 任務ID:", i)
    time.Sleep(time.Second * 1)
    wg.Done() // 每個goroutine執行完畢後就釋放等待組的計數器
}
func main() {
    countThread := 2 //runtime.NumCPU()
    for i := 0; i < countThread; i++ {
        go run(i)
    }
    wg.Add(countThread) // 需要開啟的goroutine等待組的計數器
    //等待所有的任務都釋放
    wg.Wait()
    fmt.Println("任務全部結束,退出")
}

列印結果

分析:對於等待組控制多併發的情況,只有所有的goroutine都結束了才算結束,只要有一個goroutine沒有結束, 那麼就會一直等,這顯然對資源的釋放是緩慢的;

優點:使用等待組的併發控制模型,適用於好多個goroutine協同做一件事情,因為每個goroutine做的都是這件事情的一部分,只有當全部的goroutine都完成,這件事情才算完成;
缺點:需要主動的通知某一個 goroutine 結束。
疑問:如果開啟一個後臺 goroutine 一直做事情,現在不需要了,那麼就需要通知這個goroutine 結束,否則它會一直跑。

4. 場景舉例—通道+select

針對等待組場景遺留的問題,解決辦法:
1) 設定全域性變數,在通知goroutine要停止時,為全域性變數賦值,但是這樣必須保證執行緒安 全,不可避免的必須為全域性變數加鎖,顯得有失便利;
2) 使用chan + select多路複用的方式,就會優雅許多;

package main
import (
    "fmt"
    "time"
)
func run(stop chan bool) {
    for {
        select {
        case <-stop:
            fmt.Println("任務1結束退出")
            return
        default:
            fmt.Println("任務1正在執行中")
            time.Sleep(time.Second * 2)
        }
    }
}
func main() {
    stop := make(chan bool)
    go run(stop) // 開啟goroutine
    // 執行一段時間後停止
    time.Sleep(time.Second * 10)
    fmt.Println("停止任務1。。。")
    stop <- true
    time.Sleep(time.Second * 3)
    return
}


優點:優雅、簡單
不足:如果有很多 goroutine 都需要控制結束,並且這些 goroutine 又開啟其它更多的goroutine ?

5. 場景舉例—普通context

package main
import (
    "context"
    "fmt"
    "time"
)
func run(ctx context.Context, id int) {
    for {
        select {
        case <-ctx.Done():
            fmt.Printf("任務%v結束退出\n", id)
            return
        default:
            fmt.Printf("任務%v正在執行中\n", id)
            time.Sleep(time.Second * 2)
        }
    }
}
func main() {
    //管理啟動的協程
    ctx, cancel := context.WithCancel(context.Background())
    // 開啟多個goroutine,傳入ctx
    go run(ctx, 1)
    go run(ctx, 2)
    // 執行一段時間後停止
    time.Sleep(time.Second * 10)
    fmt.Println("停止任務1")
    cancel() // 使用context的cancel函式停止goroutine
    // 為了檢測監控過是否停止,如果沒有監控輸出,表示停止
    time.Sleep(time.Second * 3)
    return
}

說明:context.Background() 返回一個空的 Context,這個空的 Context 一般用於整個 Context 樹的根節點。然後使用 context.WithCancel(parent) 函式,建立一個可取消的子 Context,然後當作引數傳給 goroutine 使用,這樣就可以使用這個子 Context 跟蹤這個 goroutine。

6. 場景舉例—Context超時

package main
import (
    "context"
    "fmt"
    "sync"
    "time"
)
func coroutine(ctx context.Context, duration time.Duration, id int, wg *sync.WaitGroup) {
    for {
        select {
        case <-ctx.Done():
            fmt.Printf("協程 %d 退出\n", id)
            wg.Done()
            return
        case <-time.After(duration):
            fmt.Printf("訊息來自協程 %d\n", id)
        }
    }
}
func main() {
    //使用WaitGroup等待所有的goroutine執行完畢,在收到<-ctx.Done()的終止訊號後使wg中需要等待的goroutine數量減一。
    // 因為context只負責取消goroutine,不負責等待goroutine執行,所以需要配合一點輔助手段
    //管理啟動的協程
    wg := &sync.WaitGroup{}
    ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
    defer cancel()
    for i := 0; i < 3; i++ {
        wg.Add(1)
        go coroutine(ctx, 1*time.Second, i, wg)
    }
    wg.Wait()
}

說明:程式碼中使用WaitGroup等待所有的goroutine執行完畢,在收到<-ctx.Done()的終止訊號後使wg中需要等待的goroutine數量減一, 因為context只負責取消goroutine,不負責等待goroutine執行,需要配合一點輔助手段。

7. 場景舉例—Context傳遞後設資料

package main
import (
    "context"
    "fmt"
    "time"
)
var key string = "name"
func run(ctx context.Context) {
    for {
        select {
        case <-ctx.Done():
            fmt.Printf("任務%v結束退出\n", ctx.Value(key))
            return
        default:
            fmt.Printf("任務%v正在執行中\n", ctx.Value(key))
            time.Sleep(time.Second * 2)
        }
    }
}
func main() {
    //管理啟動的協程
    ctx, cancel := context.WithCancel(context.Background())
    // 給ctx繫結鍵值,傳遞給goroutine
    valuectx := context.WithValue(ctx, key, "【監控1】")
    // 開啟goroutine,傳入ctx
    go run(valuectx)
    // 執行一段時間後停止
    time.Sleep(time.Second * 10)
    fmt.Println("停止任務")
    cancel() // 使用context的cancel函式停止goroutine
    // 為了檢測監控過是否停止,如果沒有監控輸出,表示停止
    time.Sleep(time.Second * 3)
}

8. context總結

1) 不要把 Context 放在結構體中,要以引數的方式傳遞
2) 以 Context 作為引數的函式方法,應該把 Context 作為第一個引數,放在第一位
3) 給一個函式方法傳遞 Context 的時候,不要傳遞 nil,如果不知道傳遞什麼,就使用 context.TODO
4) Context 的 Value 相關方法應該傳遞必須的資料,不要什麼資料都使用這個傳遞
5) Context 是執行緒安全的,可以放心的在多個 goroutine 中傳遞

本作品採用《CC 協議》,轉載必須註明作者和本文連結
good good study day day up

相關文章