Hi,你們好,我是明哥。html
在本身學習 Golang 的這段時間裏,我寫了詳細的學習筆記放在個人我的微信公衆號 《Go編程時光》,對於 Go 語言,我也算是個初學者,所以寫的東西應該會比較適合剛接觸的同窗,若是你也是剛學習 Go 語言,不防關注一下,一塊兒學習,一塊兒成長。git
個人在線博客:http://golang.iswbm.com
個人 Github:github.com/iswbm/GolangCodingTimegithub
在 Go 1.7 版本以前,context 仍是非編制的,它存在於 golang.org/x/net/context 包中。golang
後來,Golang 團隊發現 context 還挺好用的,就把 context 收編了,在 Go 1.7 版本正式歸入了標準庫。編程
Context,也叫上下文,它的接口定義以下數組
type Context interface { Deadline() (deadline time.Time, ok bool) Done() <-chan struct{} Err() error Value(key interface{}) interface{} }
能夠看到 Context 接口共有 4 個方法安全
Deadline
:返回的第一個值是 截止時間,到了這個時間點,Context 會自動觸發 Cancel 動做。返回的第二個值是 一個布爾值,true 表示設置了截止時間,false 表示沒有設置截止時間,若是沒有設置截止時間,就要手動調用 cancel 函數取消 Context。Done
:返回一個只讀的通道(只有在被cancel後纔會返回),類型爲 struct{}
。當這個通道可讀時,意味着parent context已經發起了取消請求,根據這個信號,開發者就能夠作一些清理動做,退出goroutine。Err
:返回 context 被 cancel 的緣由。Value
:返回被綁定到 Context 的值,是一個鍵值對,因此要經過一個Key才能夠獲取對應的值,這個值通常是線程安全的。當一個協程(goroutine)開啓後,咱們是沒法強制關閉它的。微信
常見的關閉協程的緣由有以下幾種:併發
第一種,屬於正常關閉,不在今天討論範圍以內。函數
第二種,屬於異常關閉,應當優化代碼。
第三種,纔是開發者能夠手動控制協程的方法,代碼示例以下:
func main() { stop := make(chan bool) go func() { for { select { case <-stop: fmt.Println("監控退出,中止了...") return default: fmt.Println("goroutine監控中...") time.Sleep(2 * time.Second) } } }() time.Sleep(10 * time.Second) fmt.Println("能夠了,通知監控中止") stop<- true //爲了檢測監控過是否中止,若是沒有監控輸出,就表示中止了 time.Sleep(5 * time.Second) }
例子中咱們定義一個stop
的chan,通知他結束後臺goroutine。實現也很是簡單,在後臺goroutine中,使用select判斷stop
是否能夠接收到值,若是能夠接收到,就表示能夠退出中止了;若是沒有接收到,就會執行default
裏的監控邏輯,繼續監控,只到收到stop
的通知。
以上是一個 goroutine 的場景,若是是多個 goroutine ,每一個goroutine 底下又開啓了多個 goroutine 的場景呢?在 飛雪無情的博客 裏關於爲什麼要使用 Context,他是這麼說的
chan+select的方式,是比較優雅的結束一個goroutine的方式,不過這種方式也有侷限性,若是有不少goroutine都須要控制結束怎麼辦呢?若是這些goroutine又衍生了其餘更多的goroutine怎麼辦呢?若是一層層的無窮盡的goroutine呢?這就很是複雜了,即便咱們定義不少chan也很難解決這個問題,由於goroutine的關係鏈就致使了這種場景很是複雜。
在這裏我不是很贊同他說的話,由於我以爲就算只使用一個通道也能達到控制(取消)多個 goroutine 的目的。下面就用例子來驗證一下。
該例子的原理是:使用 close 關閉通道後,若是該通道是無緩衝的,則它會從原來的阻塞變成非阻塞,也就是可讀的,只不過讀到的會一直是零值,所以根據這個特性就能夠判斷 擁有該通道的 goroutine 是否要關閉。
package main import ( "fmt" "time" ) func monitor(ch chan bool, number int) { for { select { case v := <-ch: // 僅當 ch 通道被 close,或者有數據發過來(不管是true仍是false)纔會走到這個分支 fmt.Printf("監控器%v,接收到通道值爲:%v,監控結束。\n", number,v) return default: fmt.Printf("監控器%v,正在監控中...\n", number) time.Sleep(2 * time.Second) } } } func main() { stopSingal := make(chan bool) for i :=1 ; i <= 5; i++ { go monitor(stopSingal, i) } time.Sleep( 1 * time.Second) // 關閉全部 goroutine close(stopSingal) // 等待5s,若此時屏幕沒有輸出 <正在監控中> 就說明全部的goroutine都已經關閉 time.Sleep( 5 * time.Second) fmt.Println("主程序退出!!") }
輸出以下
監控器4,正在監控中... 監控器1,正在監控中... 監控器2,正在監控中... 監控器3,正在監控中... 監控器5,正在監控中... 監控器2,接收到通道值爲:false,監控結束。 監控器3,接收到通道值爲:false,監控結束。 監控器5,接收到通道值爲:false,監控結束。 監控器1,接收到通道值爲:false,監控結束。 監控器4,接收到通道值爲:false,監控結束。 主程序退出!!
上面的例子,說明當咱們定義一個無緩衝通道時,若是要對全部的 goroutine 進行關閉,可使用 close 關閉通道,而後在全部的 goroutine 裏不斷檢查通道是否關閉(前提你得約定好,該通道你只會進行 close 而不會發送其餘數據,不然發送一次數據就會關閉一個goroutine,這樣會不符合我們的預期,因此最好你對這個通道再作一層封裝作個限制)來決定是否結束 goroutine。
因此你看到這裏,我作爲初學者仍是沒有找到使用 Context 的必然理由,我只能說 Context 是個很好用的東西,使用它方便了咱們在處理併發時候的一些問題,可是它並非不可或缺的。
換句話說,它解決的並非 能不能 的問題,而是解決 更好用 的問題。
若是不使用上面 close 通道的方式,還有沒有其餘更優雅的方法來實現呢?
有,那就是本文要講的 Context
我使用 Context 對上面的例子進行了一番改造。
package main import ( "context" "fmt" "time" ) func monitor(ctx context.Context, number int) { for { select { // 其實能夠寫成 case <- ctx.Done() // 這裏僅是爲了讓你看到 Done 返回的內容 case v :=<- ctx.Done(): fmt.Printf("監控器%v,接收到通道值爲:%v,監控結束。\n", number,v) return default: fmt.Printf("監控器%v,正在監控中...\n", number) time.Sleep(2 * time.Second) } } } func main() { ctx, cancel := context.WithCancel(context.Background()) for i :=1 ; i <= 5; i++ { go monitor(ctx, i) } time.Sleep( 1 * time.Second) // 關閉全部 goroutine cancel() // 等待5s,若此時屏幕沒有輸出 <正在監控中> 就說明全部的goroutine都已經關閉 time.Sleep( 5 * time.Second) fmt.Println("主程序退出!!") }
這裏面的關鍵代碼,也就三行
第一行:以 context.Background() 爲 parent context 定義一個可取消的 context
ctx, cancel := context.WithCancel(context.Background())
第二行:而後你能夠在全部的goroutine 裏利用 for + select 搭配來不斷檢查 ctx.Done() 是否可讀,可讀就說明該 context 已經取消,你能夠清理 goroutine 並退出了。
case <- ctx.Done():
第三行:當你想到取消 context 的時候,只要調用一下 cancel 方法便可。這個 cancel 就是咱們在建立 ctx 的時候返回的第二個值。
cancel()
運行結果輸出以下。能夠發現咱們實現了和 close 通道同樣的效果。
監控器3,正在監控中... 監控器4,正在監控中... 監控器1,正在監控中... 監控器2,正在監控中... 監控器2,接收到通道值爲:{},監控結束。 監控器5,接收到通道值爲:{},監控結束。 監控器4,接收到通道值爲:{},監控結束。 監控器1,接收到通道值爲:{},監控結束。 監控器3,接收到通道值爲:{},監控結束。 主程序退出!!
建立 Context 必需要指定一個 父 Context,當咱們要建立第一個Context時該怎麼辦呢?
不用擔憂,Go 已經幫咱們實現了2個,咱們代碼中最開始都是以這兩個內置的context做爲最頂層的parent context,衍生出更多的子Context。
var ( background = new(emptyCtx) todo = new(emptyCtx) ) func Background() Context { return background } func TODO() Context { return todo }
一個是Background,主要用於main函數、初始化以及測試代碼中,做爲Context這個樹結構的最頂層的Context,也就是根Context,它不能被取消。
一個是TODO,若是咱們不知道該使用什麼Context的時候,可使用這個,可是實際應用中,暫時尚未使用過這個TODO。
他們兩個本質上都是emptyCtx結構體類型,是一個不可取消,沒有設置截止時間,沒有攜帶任何值的Context。
type emptyCtx int func (*emptyCtx) Deadline() (deadline time.Time, ok bool) { return } func (*emptyCtx) Done() <-chan struct{} { return nil } func (*emptyCtx) Err() error { return nil } func (*emptyCtx) Value(key interface{}) interface{} { return nil }
上面在定義咱們本身的 Context 時,咱們使用的是 WithCancel
這個方法。
除它以外,context 包還有其餘幾個 With 系列的函數
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
這四個函數有一個共同的特色,就是第一個參數,都是接收一個 父context。
經過一次繼承,就多實現了一個功能,好比使用 WithCancel 函數傳入 根context ,就建立出了一個子 context,該子context 相比 父context,就多了一個 cancel context 的功能。
若是此時,咱們再以上面的子context(context01)作爲父context,並將它作爲第一個參數傳入WithDeadline函數,得到的子子context(context02),相比子context(context01)而言,又多出了一個超過 deadline 時間後,自動 cancel context 的功能。
接下來我會舉例介紹一下這幾種 context,其中 WithCancel 在上面已經講過了,下面就再也不舉例了
package main import ( "context" "fmt" "time" ) func monitor(ctx context.Context, number int) { for { select { case <- ctx.Done(): fmt.Printf("監控器%v,監控結束。\n", number) return default: fmt.Printf("監控器%v,正在監控中...\n", number) time.Sleep(2 * time.Second) } } } func main() { ctx01, cancel := context.WithCancel(context.Background()) ctx02, cancel := context.WithDeadline(ctx01, time.Now().Add(1 * time.Second)) defer cancel() for i :=1 ; i <= 5; i++ { go monitor(ctx02, i) } time.Sleep(5 * time.Second) if ctx02.Err() != nil { fmt.Println("監控器取消的緣由: ", ctx02.Err()) } fmt.Println("主程序退出!!") }
輸出以下
監控器5,正在監控中... 監控器1,正在監控中... 監控器2,正在監控中... 監控器3,正在監控中... 監控器4,正在監控中... 監控器3,監控結束。 監控器4,監控結束。 監控器2,監控結束。 監控器1,監控結束。 監控器5,監控結束。 監控器取消的緣由: context deadline exceeded 主程序退出!!
WithTimeout 和 WithDeadline 使用方法及功能基本一致,都是表示超過必定的時間會自動 cancel context。
惟一不一樣的地方,咱們能夠從函數的定義看出
func WithDeadline(parent Context, deadline time.Time) (Context, CancelFunc) func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc)
WithDeadline 傳入的第二個參數是 time.Time 類型,它是一個絕對的時間,意思是在什麼時間點超時取消。
而 WithTimeout 傳入的第二個參數是 time.Duration 類型,它是一個相對的時間,意思是多長時間後超時取消。
package main import ( "context" "fmt" "time" ) func monitor(ctx context.Context, number int) { for { select { case <- ctx.Done(): fmt.Printf("監控器%v,監控結束。\n", number) return default: fmt.Printf("監控器%v,正在監控中...\n", number) time.Sleep(2 * time.Second) } } } func main() { ctx01, cancel := context.WithCancel(context.Background()) // 相比例子1,僅有這一行改動 ctx02, cancel := context.WithTimeout(ctx01, 1* time.Second) defer cancel() for i :=1 ; i <= 5; i++ { go monitor(ctx02, i) } time.Sleep(5 * time.Second) if ctx02.Err() != nil { fmt.Println("監控器取消的緣由: ", ctx02.Err()) } fmt.Println("主程序退出!!") }
輸出的結果和上面同樣
監控器1,正在監控中... 監控器5,正在監控中... 監控器3,正在監控中... 監控器2,正在監控中... 監控器4,正在監控中... 監控器4,監控結束。 監控器2,監控結束。 監控器5,監控結束。 監控器1,監控結束。 監控器3,監控結束。 監控器取消的緣由: context deadline exceeded 主程序退出!!
經過Context咱們也能夠傳遞一些必須的元數據,這些數據會附加在Context上以供使用。
元數據以 Key-Value 的方式傳入,Key 必須有可比性,Value 必須是線程安全的。
仍是用上面的例子,以 ctx02 爲父 context,再建立一個能攜帶 value 的ctx03,因爲他的父context 是 ctx02,因此 ctx03 也具有超時自動取消的功能。
package main import ( "context" "fmt" "time" ) func monitor(ctx context.Context, number int) { for { select { case <- ctx.Done(): fmt.Printf("監控器%v,監控結束。\n", number) return default: // 獲取 item 的值 value := ctx.Value("item") fmt.Printf("監控器%v,正在監控 %v \n", number, value) time.Sleep(2 * time.Second) } } } func main() { ctx01, cancel := context.WithCancel(context.Background()) ctx02, cancel := context.WithTimeout(ctx01, 1* time.Second) ctx03 := context.WithValue(ctx02, "item", "CPU") defer cancel() for i :=1 ; i <= 5; i++ { go monitor(ctx03, i) } time.Sleep(5 * time.Second) if ctx02.Err() != nil { fmt.Println("監控器取消的緣由: ", ctx02.Err()) } fmt.Println("主程序退出!!") }
輸出以下
監控器4,正在監控 CPU 監控器5,正在監控 CPU 監控器1,正在監控 CPU 監控器3,正在監控 CPU 監控器2,正在監控 CPU 監控器2,監控結束。 監控器5,監控結束。 監控器3,監控結束。 監控器1,監控結束。 監控器4,監控結束。 監控器取消的緣由: context deadline exceeded 主程序退出!!
系列導讀
24. 超詳細解讀 Go Modules 前世此生及入門使用