golang通過context控制併發的應用場景實現
golang 裡出現多 goroutine 的場景很常見,最常用的兩種方式就是 WaitGroup 和 Context,今天我們瞭解一下 Context 的應用場景
使用場景
場景一: 多goroutine執行超時通知
併發執行的業務中最常見的就是有協程執行超時,如果不做超時處理就會出現一個殭屍程序,這累計的多了就會有一陣手忙腳亂了,所以我們要在源頭上就避免它們
看下面這個示例:
package main import ( "context" "fmt" "time" ) /** 同一個content可以控制多個goroutine,確保執行緒可控,而不是每新建一個goroutine就要有一個chan去通知他關閉 有了他程式碼更加簡潔 */ func main() { fmt.Println("run demo \n\n\n") demo() } func demo() { ctx,cancel := context.WithTimeout(context.Background(),9*time.Second) go watch(ctx,"[執行緒1]") go watch(ctx,"[執行緒2]") go watch(ctx,"[執行緒3]") index := 0 for { index++ fmt.Printf("%d 秒過去了 \n",index) time.Sleep(1 * time.Second) if index > 10 { break } } fmt.Println("通知停止監控") // 其實此時已經超時,協程已經提前退出 cancel() // 防止主程序提前退出 time.Sleep(3 * time.Second) fmt.Println("done") } func watch(ctx context.Context,name string) { for { select { case <-ctx.Done(): fmt.Printf("%s 監控退出,停止了...\n",name) return default: fmt.Printf("%s goroutine監控中... \n",name) time.Sleep(2 * time.Second) } } }
使用 context.WithTimeout() 給文字流設定一個時間上限,結合 for+select 去接收訊息. 當執行超時,或手動關閉都會給 <-ctx.Done() 傳送訊息,而且所有使用同一個 context 都會收到這個通知,免去了一個一個通知的繁瑣程式碼
場景二: 類似web伺服器中的session
比如在php中(沒用swoole擴充套件),一個請求進來,從 $_REQUEST $_SERVER 能獲取到的是有關這一條請求的所有資訊,哪怕是使用全域性變數也是給這一個請求來服務的,是執行緒安全的
但是 golang 就不一樣了,因為程式本身就能起一個 web sever,因此就不能隨便使用全域性變量了,不然就是記憶體洩露警告. 但是實際業務當中需要有一個類似session 的東西來承載單次請求的資訊,舉一個具體的例子就是: 給每次請求加一個 uniqueID 該如何處理? 有了這個 uniqueID,請求的所有日誌都能帶上它,這樣排查問題的時候方便追蹤一次請求發生了什麼
如下:
func demo2() { pCtx,pCancel := context.WithCancel(context.Background()) pCtx = context.WithValue(pCtx,"parentKey","parentVale") go watch(pCtx,"[父程序1]") go watch(pCtx,"[父程序2]") cCtx,cCancel := context.WithCancel(pCtx) go watch(cCtx,"[子程序1]") go watch(cCtx,"[子程序2]") fmt.Println(pCtx.Value("parentKey")) fmt.Println(cCtx.Value("parentKey")) time.Sleep(10 * time.Second) fmt.Println("子程序關閉") cCancel() time.Sleep(5 * time.Second) fmt.Println("父程序關閉") pCancel() time.Sleep(3 * time.Second) fmt.Println("done") }
最開始的 context.WithCancel(context.Background()) 中 context.Background() 就是一個新建的 context,利用 context 能繼承的特性,可以將自己的程式構建出一個 context 樹,context 執行 cancel() 將影響到當前 context 和子 context,不會影響到父級.
同時 context.WithValue 也會給 context 帶上自定義的值,這樣 uniqueID 就能輕鬆的傳遞了下去,而不是一層層的傳遞引數,改func什麼的
對於 context 很值得參考的應用有:
- Gin
- logrus
Context 相關 func 和介面
繼承 context 需要實現如下四個介面
type Context interface { Deadline() (deadline time.Time,ok bool) Done() <-chan struct{} Err() error Value(key interface{}) interface{} }
當使用的時候不需要實現介面,因為官方包裡已經基於 emptyCtx 實現了一個,呼叫方法有
var ( background = new(emptyCtx) todo = new(emptyCtx) ) // 這個是最初始的ctx,之後的子ctx都是繼承自它 func Background() Context { return background } // 不清楚context要幹嘛,但是就得有一個ctx的用這個 func TODO() Context { return todo }
繼承用的函式
func WithCancel(parent Context) (ctx Context,cancel CancelFunc) func WithDeadline(parent Context,deadline time.Time) (Context,CancelFunc) func WithTimeout(parent Context,timeout time.Duration) (Context,CancelFunc) func WithValue(parent Context,key,val interface{}) Context
- WithCancel 返回一個帶 cancel 函式的ctx,
- WithDeadline 在到達指定時間時自動執行 cancel()
- WithTimeout 是 WithDeadline的殼子,區別就是這個函式是多少時間過後執行 cancel
func WithTimeout(parent Context,CancelFunc) { return WithDeadline(parent,time.Now().Add(timeout)) }
WithValue 繼承父類ctx時順便帶上一個值
以上就是本文的全部內容,希望對大家的學習有所幫助,也希望大家多多支援我們。