您好,登錄后才能下訂單哦!
本篇內容介紹了“Go語言并發編程基礎上下文概念是什么”的有關知識,在實際案例的操作過程中,不少人都會遇到這樣的困境,接下來就讓小編帶領大家學習一下如何處理這些情況吧!希望大家仔細閱讀,能夠學有所成!
Golang 的上下文也是應用開發常用的并發控制工具。同理,上下文可以用于在程序中的 API 層或進程之間共享請求范圍的數據,除此之外,Go 的 Context 庫還提供取消信號(Cancel)以及超時機制(Timeout)。
Context 又被稱為上下文,與 WaitGroup 不同的是,Context 對于派生 goroutine 有更強的控制力,可以管理多級的 goroutine。
但我們在 Go 中創建一個 goroutine 時,如果發生了一個錯誤,并且這個錯誤永遠不會終止,而其他程序會繼續進行。加入有一個不被調用的 goroutine 運行無限循環,如下所示:
package main import "fmt" func main() { dataCom := []string{"alex", "kyrie", "kobe"} go func(data []string) { // 模擬大量運算的死循環 }(dataCom) // 其他代碼正常執行 fmt.Println("剩下的代碼執行正常邏輯") }
上面的例子并不完整,dataCom
goroutine 可能會也可能不會成功處理數據。它可能會進入無限循環或導致錯誤。我們的其余代碼將不知道發生了什么。
有多種方法可以解決這個問題。其中之一是使用通道向我們的主線程發送一個信號,表明這個 goroutine 花費的時間太長,應該取消它。
package main import ( "fmt" "time" ) func main() { stopChannel := make(chan bool) dataCom := []string{"alex", "kyrie", "kobe"} go func(stopChannel chan bool) { go func(data []string) { // 大量的計算 }(dataCom) for range time.After(2 * time.Second) { fmt.Println("此操作運行時間過長,取消中") stopChannel <- true } }(stopChannel) <-stopChannel // 其他代碼正常執行 fmt.Println("剩下的代碼執行正常邏輯") }
上面的邏輯很簡單。我們正在使用一個通道向我們的主線程發出這個 goroutine 花費的時間太長的信號。但是同樣的事情可以用 context 來完成,這正是 context 包存在的原因。
package main import ( "context" "fmt" "time" ) func main() { ctx, cancel := context.WithTimeout(context.Background(), time.Second*2) defer cancel() dataCom := []string{"alex", "kyrie", "kobe"} go func() { go func(data []string) { // 大量的計算 }(dataCom) for range time.After(2 * time.Second) { fmt.Println("此操作運行時間過長,取消中") cancel() return } }() select { case <-ctx.Done(): fmt.Println("上下文被取消") } }
Context 接口定義:
type Context interface { Deadline() (deadline time.Time, ok bool) Done <-chan struct{} Err() error Value(key interface{}) interface{} }
Context 接口定義了 4 個方法:
Deadline()
: 返回取消此上下文的時間 deadline(如果有)。如果未設置 deadline 時,則返回 ok==false,此時 deadline 為一個初始值的 time.Time 值。后續每次調用這個對象的 Deadline 方法時,都會返回和第一次調用相同的結果。
Done()
: 返回一個用于探測 Context 是否取消的 channel,當 Context 取消會自動將該 channel 關閉,如果該 Context 不能被永久取消,該函數返回 nil。例如 context.Background()
;如果 Done
被 close,Err 方法會返回 Done 被 close 的原因。
Err()
: 該方法會返回 context 被關閉的原因,關閉原因由 context 實現控制,不需要用戶設置;如果 Done()
尚未關閉,則 Err()
返回 nil
Value()
: 在樹狀分布的 goroutine
之間共享數據,用 map 鍵值的工作方法,通過 key 值查詢 value。
每次創建新上下文時,都會得到一個符合此接口的類型。上下文的真正實現隱藏在這個包和這個接口后面。這些是您可以創建的工廠類型的上下文:
context.TODO
context.Background
context.WithCancel
context.WithValue
context.WithTimeout
context.WithDeadline
在實際實現中,我們通常使用派生上下文。我們創建一個父上下文并將其傳遞到一個層,我們派生一個新的上下文,它添加一些額外的信息并將其再次傳遞到下一層,依此類推。通過這種方式,我們創建了一個從作為父級的根上下文開始的上下文樹。
這種結構的優點是我們可以一次性控制所有上下文的取消。如果根信號關閉了上下文,它將在所有派生的上下文中傳播,這些上下文可用于終止所有進程,立即釋放所有內容。這使得上下文成為并發編程中非常強大的工具。
我們可以從現有的上下文中創建或派生上下文。頂層(根)上下文是使用 Background
或 TODO
方法創建的,而派生上下文是使用 WithCancel、WithDeadline、WithTimeout 或 WithValue 方法創建的。
所有派生的上下文方法都返回一個取消函數 CancelFunc,但 WithValue 除外,因為它與取消無關。調用 CancelFunc 會取消子項及其子項,刪除父項對子項的引用,并停止任何關聯的計時器。調用 CancelFunc 失敗會泄漏子項及其子項,直到父項被取消或計時器觸發。
context.Background() ctx Context
此函數返回一個空上下文。這通常只應在主請求處理程序或頂級請求處理程序中使用。這可用于為主函數、初始化、測試以及后續層或其他 goroutine 派生上下文的時候。
ctx, cancel := context.Background()
context.TODO() ctx Context
此函數返回一個非 nil 的、空的上下文。沒有任何值、不會被 cancel,不會超時,也沒有截止日期。但是,這也應該僅在您不確定要使用什么上下文或者該函數還不能用于接收上下文時,可以使用這個方法,并且將在將來需要添加時使用。
ctx, cancel := context.TODO()
context.WithValue(parent Context, key, val interface{}) Context
這個函數接受一個上下文并返回一個派生的上下文,其中值 val 與 key 相關聯,并與上下文一起經過上下文樹。
WithValue
方法其實是創建了一個類型為 valueCtx 的上下文,它的類型定義如下:
type valueCtx struct { Context key, val interface{} }
這意味著一旦你得到一個帶有值的上下文,任何從它派生的上下文都會得到這個值。該值是不可變的,因此是線程安全的。
提供的鍵必須是可比較的,并且不應該是字符串類型或任何其他內置類型,以避免使用上下文的包之間發生沖突。 WithValue 的用戶應該為鍵定義自己的類型。
為避免在分配給 interface{}
時進行分配,上下文鍵通常具有具體類型 struct{}
。或者,導出的上下文鍵變量的靜態類型應該是指針或接口。
package main import ( "context" "fmt" ) type contextKey string func main() { var authToken contextKey = "auth_token" ctx := context.WithValue(context.Background(), authToken, "Hello123456") fmt.Println(ctx.Value(authToken)) }
運行該代碼:
$ go run .
Hello123456
func WithCancel(parent Context) (ctx Context, cancel CancelFunc)
此函數接收父上下文并返回派生上下文,返回 parent 的副本,只是副本中的 Done Channel 是新建的對象,它的類型是 cancelCtx。在這個派生上下文中,添加了一個新的 Done
channel,該 channel 在調用 cancel
函數或父上下文的 Done 通道關閉時關閉。
要記住的一件事是,我們永遠不應該在不同的函數或層之間傳遞這個 cancel
,因為它可能會導致意想不到的結果。創建派生上下文的函數應該只調用取消函數。
下面是一個使用 Done 通道演示 goroutine 泄漏的示例:
package main import ( "context" "fmt" "math/rand" "time" ) func main() { rand.Seed(time.Now().UnixNano()) ctx, cancel := context.WithCancel(context.Background()) defer cancel() for char := range randomCharGenerator(ctx) { generatedChar := string(char) fmt.Printf("%v\n", generatedChar) if generatedChar == "o" { break } } } func randomCharGenerator(ctx context.Context) <-chan int { char := make(chan int) seedChar := int('a') go func() { for { select { case <-ctx.Done(): fmt.Printf("found %v", seedChar) return case char <- seedChar: seedChar = 'a' + rand.Intn(26) } } }() return char }
運行結果:
$ go run .
a
m
q
c
l
t
o
func WithDeadline(parent Context, d time.Time) (Context, CancelFunc)
此函數從其父級返回派生上下文,返回一個 parent 的副本。
當期限超過或調用取消函數時,該派生上下文將被取消。例如,您可以創建一個在未來某個時間自動取消的上下文,并將其傳遞給子函數。當該上下文由于截止日期用完而被取消時,所有獲得該上下文的函數都會收到通知停止工作并返回。如果 parent 的截止日期已經早于 d,則上下文的 Done 通道已經關閉。
下面是我們正在讀取一個大文件的示例,該文件的截止時間為當前時間 2 毫秒。我們將獲得 2 毫秒的輸出,然后將關閉上下文并退出程序。
package main import ( "bufio" "context" "fmt" "log" "os" "time" ) func main() { // context with deadline after 2 millisecond ctx, cancel := context.WithDeadline(context.Background(), time.Now().Add(2*time.Millisecond)) defer cancel() lineRead := make(chan string) var fileName = "sample-file.txt" file, err := os.Open(fileName) if err != nil { log.Fatalf("failed opening file: %s", err) } scanner := bufio.NewScanner(file) scanner.Split(bufio.ScanLines) // goroutine to read file line by line and passing to channel to print go func() { for scanner.Scan() { lineRead <- scanner.Text() } close(lineRead) file.Close() }() outer: for { // printing file line by line until deadline is reached select { case <-ctx.Done(): fmt.Println("process stopped. reason: ", ctx.Err()) break outer case line := <-lineRead: fmt.Println(line) } } }
func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc)
這個函數類似于 context.WithDeadline。不同之處在于它將持續時間作為輸入而不是時間對象。此函數返回一個派生上下文,如果調用取消函數或超過超時持續時間,該上下文將被取消。
WithTimeout 的實現是:
func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc) { // 當前時間+timeout就是deadline return WithDeadline(parent, time.Now().Add(timeout)) }
WithTimeout 返回 WithDeadline(parent, time.Now().Add(timeout))
。
package main import ( "bufio" "context" "fmt" "log" "os" "time" ) func main() { // context with deadline after 2 millisecond ctx, cancel := context.WithTimeout(context.Background(), 2*time.Millisecond) defer cancel() lineRead := make(chan string) var fileName = "sample-file.txt" file, err := os.Open(fileName) if err != nil { log.Fatalf("failed opening file: %s", err) } scanner := bufio.NewScanner(file) scanner.Split(bufio.ScanLines) // goroutine to read file line by line and passing to channel to print go func() { for scanner.Scan() { lineRead <- scanner.Text() } close(lineRead) file.Close() }() outer: for { // printing file line by line until deadline is reached select { case <-ctx.Done(): fmt.Println("process stopped. reason: ", ctx.Err()) break outer case line := <-lineRead: fmt.Println(line) } } }
如果父上下文的 Done 通道關閉,它最終將關閉所有派生的 Done 通道(所有后代),如:
package main import ( "context" "fmt" "time" ) func main() { c := make(chan string) go func() { time.Sleep(1 * time.Second) c <- "one" }() ctx1 := context.Context(context.Background()) ctx2, cancel2 := context.WithTimeout(ctx1, 2*time.Second) ctx3, cancel3 := context.WithTimeout(ctx2, 10*time.Second) // derives from ctx2 ctx4, cancel4 := context.WithTimeout(ctx2, 3*time.Second) // derives from ctx2 ctx5, cancel5 := context.WithTimeout(ctx4, 5*time.Second) // derives from ctx4 cancel2() defer cancel3() defer cancel4() defer cancel5() select { case <-ctx3.Done(): fmt.Println("ctx3 closed! error: ", ctx3.Err()) case <-ctx4.Done(): fmt.Println("ctx4 closed! error: ", ctx4.Err()) case <-ctx5.Done(): fmt.Println("ctx5 closed! error: ", ctx5.Err()) case msg := <-c: fmt.Println("received", msg) } }
在這里,由于我們在創建其他派生上下文后立即關閉 ctx2,因此所有其他上下文也會立即關閉,隨機打印 ctx3、ctx4 和 ctx5 關閉消息。 ctx5 是從 ctx4 派生的,由于 ctx2 關閉的級聯效應,它正在關閉。嘗試多次運行,您會看到不同的結果。
使用 Background 或 TODO 方法創建的上下文沒有取消、值或截止日期。
package main import ( "context" "fmt" ) func main() { ctx := context.Background() _, ok := ctx.Deadline() if !ok { fmt.Println("no dealine is set") } done := ctx.Done() if done == nil { fmt.Println("channel is nil") } }
不要將上下文存儲在結構類型中;相反,將 Context 顯式傳遞給需要它的每個函數。 Context 應該是第一個參數,通常命名為 ctx。
func DoSomething(ctx context.Context, arg Arg) error { // ... use ctx ... }
不要傳遞 nil 上下文,即使函數允許。如果不確定要使用哪個 Context,請傳遞 context.TODO
或使用 context.Background()
創建一個空的上下文對象。
僅使用上下文傳遞請求范圍的數據。不要傳遞應該使用函數參數傳遞的數據。
始終尋找 goroutine 泄漏并有效地使用上下文來避免這種情況。
如果父上下文的 Done 通道關閉,它最終將關閉所有派生的 Done 通道(所有后代)
上下文只是臨時做函數之間的上下文傳透,不能持久化上下文
key 的類型不應該是字符串類型或者其它內建類型,否則容易在包之間使用 Context 時候產生沖突。使用 WithValue 時,key 的類型應該是自己定義的類型。
上下文信息傳遞 (request-scoped),比如處理 http 請求、在請求處理鏈路上傳遞信息;
控制子 goroutine 的運行;
超時控制的方法調用;
可以取消的方法調用。
“Go語言并發編程基礎上下文概念是什么”的內容就介紹到這里了,感謝大家的閱讀。如果想了解更多行業相關的知識可以關注億速云網站,小編將為大家輸出更多高質量的實用文章!
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。