<em>Mac</em>Book项目 2009年学校开始实施<em>Mac</em>Book项目,所有师生配备一本<em>Mac</em>Book,并同步更新了校园无线网络。学校每周进行电脑技术更新,每月发送技术支持资料,极大改变了教学及学习方式。因此2011
2021-06-01 09:32:01
相信大家以前在做閱讀理解的時候,一定有從老師那裡學一個技巧或者從參考答案看個:結合上下文。根據上下文我們能夠找到有助於解題的相關資訊,也能更加了解段落的思想。
在開發過程中,也有這個上下文(Context)的概念,而且上下文也必不可少,缺少上下文,就不能獲取完整的程式資訊。那麼什麼是程式中的上下文呢?
簡單來說,就是在 API 之間或者函數呼叫之間,除了業務引數資訊之外的額外資訊。比如,伺服器接收到使用者端的 HTTP 請求之後,可以把使用者端的 IP 地址和埠、使用者端的身份資訊、請求接收的時間、Trace ID 等資訊放入到上下文中,這個上下文可以在後端的方法呼叫中傳遞。
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()
返回 nilValue()
: 在樹狀分佈的 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 失敗會洩漏子項及其子項,直到父項被取消或計時器觸發。
此函數返回一個空上下文。這通常只應在主請求處理程式或頂級請求處理程式中使用。這可用於為主函數、初始化、測試以及後續層或其他 goroutine 派生上下文的時候。
ctx, cancel := context.Background()
此函數返回一個非 nil 的、空的上下文。沒有任何值、不會被 cancel,不會超時,也沒有截止日期。但是,這也應該僅在您不確定要使用什麼上下文或者該函數還不能用於接收上下文時,可以使用這個方法,並且將在將來需要新增時使用。
ctx, cancel := context.TODO()
這個函數接受一個上下文並返回一個派生的上下文,其中值 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
此函數接收父上下文並返回派生上下文,返回 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("%vn", 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
此函數從其父級返回派生上下文,返回一個 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) } } }
這個函數類似於 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") } }
func DoSomething(ctx context.Context, arg Arg) error { // ... use ctx ... }
context.TODO
或使用 context.Background()
建立一個空的上下文物件。Context 是在 Go 中進行並行程式設計時最重要的工具之一。上下文的主要作用是在多個 Goroutine 或者模組之間同步取消訊號或者截止日期,用於減少對資源的消耗和長時間佔用,避免資源浪費。標準庫中的 database/sql、os/exec、net、net/http 等包中都使用到了 Context。
參考連結:
Go Concurrency Patterns: Context
以上就是Go語言並行程式設計基礎上下文概念詳解的詳細內容,更多關於Go語言並行上下文的資料請關注it145.com其它相關文章!
相關文章
<em>Mac</em>Book项目 2009年学校开始实施<em>Mac</em>Book项目,所有师生配备一本<em>Mac</em>Book,并同步更新了校园无线网络。学校每周进行电脑技术更新,每月发送技术支持资料,极大改变了教学及学习方式。因此2011
2021-06-01 09:32:01
综合看Anker超能充系列的性价比很高,并且与不仅和iPhone12/苹果<em>Mac</em>Book很配,而且适合多设备充电需求的日常使用或差旅场景,不管是安卓还是Switch同样也能用得上它,希望这次分享能给准备购入充电器的小伙伴们有所
2021-06-01 09:31:42
除了L4WUDU与吴亦凡已经多次共事,成为了明面上的厂牌成员,吴亦凡还曾带领20XXCLUB全队参加2020年的一场音乐节,这也是20XXCLUB首次全员合照,王嗣尧Turbo、陈彦希Regi、<em>Mac</em> Ova Seas、林渝植等人全部出场。然而让
2021-06-01 09:31:34
目前应用IPFS的机构:1 谷歌<em>浏览器</em>支持IPFS分布式协议 2 万维网 (历史档案博物馆)数据库 3 火狐<em>浏览器</em>支持 IPFS分布式协议 4 EOS 等数字货币数据存储 5 美国国会图书馆,历史资料永久保存在 IPFS 6 加
2021-06-01 09:31:24
开拓者的车机是兼容苹果和<em>安卓</em>,虽然我不怎么用,但确实兼顾了我家人的很多需求:副驾的门板还配有解锁开关,有的时候老婆开车,下车的时候偶尔会忘记解锁,我在副驾驶可以自己开门:第二排设计很好,不仅配置了一个很大的
2021-06-01 09:30:48
不仅是<em>安卓</em>手机,苹果手机的降价力度也是前所未有了,iPhone12也“跳水价”了,发布价是6799元,如今已经跌至5308元,降价幅度超过1400元,最新定价确认了。iPhone12是苹果首款5G手机,同时也是全球首款5nm芯片的智能机,它
2021-06-01 09:30:45