1. 程式人生 > 程式設計 >golang通過context控制併發的應用場景實現

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時順便帶上一個值

以上就是本文的全部內容,希望對大家的學習有所幫助,也希望大家多多支援我們。