控制併發有兩種經典的方式,一種是WaitGroup,另一種就是Context,今天我就談談Context。安全
WaitGroup之前咱們在併發的時候介紹過,它是一種控制併發的方式,它的這種方式是控制多個goroutine同時完成。網絡
func main() { var wg sync.WaitGroup wg.Add(2) go func() { time.Sleep(2*time.Second) fmt.Println("1號完成") wg.Done() }() go func() { time.Sleep(2*time.Second) fmt.Println("2號完成") wg.Done() }() wg.Wait() fmt.Println("好了,你們都幹完了,放工") }
這是一種控制併發的方式,這種尤爲適用於,好多個goroutine協同作一件事情的時候,由於每一個goroutine作的都是這件事情的一部分,只有所有的goroutine都完成,這件事情纔算是完成,這是等待的方式。一個很簡單的例子,必定要例子中的2個goroutine同時作完,纔算是完成,先作好的就要等着其餘未完成的,全部的goroutine要都所有完成才能夠。多線程
在實際的業務種,咱們可能會有這麼一種場景:須要咱們主動的通知某一個goroutine結束。好比咱們開啓一個後臺goroutine一直作事情,好比監控,如今不須要了,就須要通知這個監控goroutine結束,否則它會一直跑,就泄漏了。併發
咱們都知道一個goroutine啓動後,咱們是沒法控制他的,大部分狀況是等待它本身結束,那麼若是這個goroutine是一個不會本身結束的後臺goroutine呢?好比監控等,會一直運行的。函數
這種狀況化,一直傻瓜式的辦法是全局變量,其餘地方經過修改這個變量完成結束通知,而後後臺goroutine不停的檢查這個變量,若是發現被通知關閉了,就自我結束。測試
這種方式也能夠,可是首先咱們要保證這個變量在多線程下的安全,基於此,有一種更好的方式:chan + select 。網站
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) }
有了以上的邏輯,咱們就能夠在其餘goroutine種,給stop
chan發送值了,例子中是在main goroutine中發送的,控制讓這個監控的goroutine結束。例子中咱們定義一個stop
的chan,通知他結束後臺goroutine。實現也很是簡單,在後臺goroutine中,使用select判斷stop
是否能夠接收到值,若是能夠接收到,就表示能夠退出中止了;若是沒有接收到,就會執行default
裏的監控邏輯,繼續監控,只到收到stop
的通知。spa
發送了stop<- true
結束的指令後,我這裏使用time.Sleep(5 * time.Second)
故意停頓5秒來檢測咱們結束監控goroutine是否成功。若是成功的話,不會再有goroutine監控中...
的輸出了;若是沒有成功,監控goroutine就會繼續打印goroutine監控中...
輸出。線程
這種chan+select的方式,是比較優雅的結束一個goroutine的方式,不過這種方式也有侷限性,若是有不少goroutine都須要控制結束怎麼辦呢?若是這些goroutine又衍生了其餘更多的goroutine怎麼辦呢?若是一層層的無窮盡的goroutine呢?這就很是複雜了,即便咱們定義不少chan也很難解決這個問題,由於goroutine的關係鏈就致使了這種場景很是複雜。code
上面說的這種場景是存在的,好比一個網絡請求Request,每一個Request都須要開啓一個goroutine作一些事情,這些goroutine又可能會開啓其餘的goroutine。因此咱們須要一種能夠跟蹤goroutine的方案,才能夠達到控制他們的目的,這就是Go語言爲咱們提供的Context,稱之爲上下文很是貼切,它就是goroutine的上下文。
下面咱們就使用Go Context重寫上面的示例。
func main() { ctx, cancel := context.WithCancel(context.Background()) go func(ctx context.Context) { for { select { case <-ctx.Done(): fmt.Println("監控退出,中止了...") return default: fmt.Println("goroutine監控中...") time.Sleep(2 * time.Second) } } }(ctx) time.Sleep(10 * time.Second) fmt.Println("能夠了,通知監控中止") cancel() //爲了檢測監控過是否中止,若是沒有監控輸出,就表示中止了 time.Sleep(5 * time.Second) }
context.Background()
返回一個空的Context,這個空的Context通常用於整個Context樹的根節點。而後咱們使用context.WithCancel(parent)
函數,建立一個可取消的子Context,而後看成參數傳給goroutine使用,這樣就可使用這個子Context跟蹤這個goroutine。重寫比較簡單,就是把原來的chan stop
換成Context,使用Context跟蹤goroutine,以便進行控制,好比結束等。
在goroutine中,使用select調用<-ctx.Done()
判斷是否要結束,若是接受到值的話,就能夠返回結束goroutine了;若是接收不到,就會繼續進行監控。
那麼是如何發送結束指令的呢?這就是示例中的cancel
函數啦,它是咱們調用context.WithCancel(parent)
函數生成子Context的時候返回的,第二個返回值就是這個取消函數,它是CancelFunc
類型的。咱們調用它就能夠發出取消指令,而後咱們的監控goroutine就會收到信號,就會返回結束。
使用Context控制一個goroutine的例子如上,很是簡單,下面咱們看看控制多個goroutine的例子,其實也比較簡單。
func main() { ctx, cancel := context.WithCancel(context.Background()) go watch(ctx,"【監控1】") go watch(ctx,"【監控2】") go watch(ctx,"【監控3】") time.Sleep(10 * time.Second) fmt.Println("能夠了,通知監控中止") cancel() //爲了檢測監控過是否中止,若是沒有監控輸出,就表示中止了 time.Sleep(5 * time.Second) } func watch(ctx context.Context, name string) { for { select { case <-ctx.Done(): fmt.Println(name,"監控退出,中止了...") return default: fmt.Println(name,"goroutine監控中...") time.Sleep(2 * time.Second) } } }
《Go語言實戰》讀書筆記,未完待續,歡迎掃碼關注公衆號flysnow_org
或者網站http://www.flysnow.org/,第一時間看後續筆記。以爲有幫助的話,順手分享到朋友圈吧,感謝支持。示例中啓動了3個監控goroutine進行不斷的監控,每個都使用了Context進行跟蹤,當咱們使用cancel
函數通知取消時,這3個goroutine都會被結束。這就是Context的控制能力,它就像一個控制器同樣,按下開關後,全部基於這個Context或者衍生的子Context都會收到通知,這時就能夠進行清理操做了,最終釋放goroutine,這就優雅的解決了goroutine啓動後不可控的問題。
Context的接口定義的比較簡潔,咱們看下這個接口的方法。
type Context interface { Deadline() (deadline time.Time, ok bool) Done() <-chan struct{} Err() error Value(key interface{}) interface{} }
Deadline
方法是獲取設置的截止時間的意思,第一個返回式是截止時間,到了這個時間點,Context會自動發起取消請求;第二個返回值ok==false時表示沒有設置截止時間,若是須要取消的話,須要調用取消函數進行取消。這個接口共有4個方法,瞭解這些方法的意思很是重要,這樣咱們才能夠更好的使用他們。
Done
方法返回一個只讀的chan,類型爲struct{}
,咱們在goroutine中,若是該方法返回的chan能夠讀取,則意味着parent context已經發起了取消請求,咱們經過Done
方法收到這個信號後,就應該作清理操做,而後退出goroutine,釋放資源。
Err
方法返回取消的錯誤緣由,由於什麼Context被取消。
Value
方法獲取該Context上綁定的值,是一個鍵值對,因此要經過一個Key才能夠獲取對應的值,這個值通常是線程安全的。
以上四個方法中經常使用的就是Done
了,若是Context取消的時候,咱們就能夠獲得一個關閉的chan,關閉的chan是能夠讀取的,因此只要能夠讀取的時候,就意味着收到Context取消的信號了,如下是這個方法的經典用法。
func Stream(ctx context.Context, out chan<- Value) error { for { v, err := DoSomething(ctx) if err != nil { return err } select { case <-ctx.Done(): return ctx.Err() case out <- v: } } }
|
Context接口並不須要咱們實現,Go內置已經幫咱們實現了2個,咱們代碼中最開始都是以這兩個內置的做爲最頂層的partent context,衍生出更多的子Context。
var ( background = new(emptyCtx) todo = new(emptyCtx) ) func Background() Context { return background } func TODO() Context { return todo }
一個是TODO
,它目前還不知道具體的使用場景,若是咱們不知道該使用什麼Context的時候,可使用這個。一個是Background
,主要用於main函數、初始化以及測試代碼中,做爲Context這個樹結構的最頂層的Context,也就是根Context。
他們兩個本質上都是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的繼承衍生
這就是emptyCtx
實現Context接口的方法,能夠看到,這些方法什麼都沒作,返回的都是nil或者零值。
有了如上的根Context,那麼是如何衍生更多的子Context的呢?這就要靠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
這四個With
函數,接收的都有一個partent參數,就是父Context,咱們要基於這個父Context建立出子Context的意思,這種方式能夠理解爲子Context對父Context的繼承,也能夠理解爲基於父Context的衍生。
經過這些函數,就建立了一顆Context樹,樹的每一個節點均可以有任意多個子節點,節點層級能夠有任意多個。
WithCancel
函數,傳遞一個父Context做爲參數,返回子Context,以及一個取消函數用來取消Context。WithDeadline
函數,和WithCancel
差很少,它會多傳遞一個截止時間參數,意味着到了這個時間點,會自動取消Context,固然咱們也能夠不等到這個時候,能夠提早經過取消函數進行取消。
WithTimeout
和WithDeadline
基本上同樣,這個表示是超時自動取消,是多少時間後自動取消Context的意思。
WithValue
函數和取消Context無關,它是爲了生成一個綁定了一個鍵值對數據的Context,這個綁定的數據能夠經過Context.Value
方法訪問到,後面咱們會專門講。
你們可能留意到,前三個函數都返回一個取消函數CancelFunc
,這是一個函數類型,它的定義很是簡單。
type CancelFunc func()
WithValue傳遞元數據這就是取消函數的類型,該函數能夠取消一個Context,以及這個節點Context下全部的全部的Context,無論有多少層級。
經過Context咱們也能夠傳遞一些必須的元數據,這些數據會附加在Context上以供使用。
var key string="name" func main() { ctx, cancel := context.WithCancel(context.Background()) //附加值 valueCtx:=context.WithValue(ctx,key,"【監控1】") go watch(valueCtx) time.Sleep(10 * time.Second) fmt.Println("能夠了,通知監控中止") cancel() //爲了檢測監控過是否中止,若是沒有監控輸出,就表示中止了 time.Sleep(5 * time.Second) } func watch(ctx context.Context) { for { select { case <-ctx.Done(): //取出值 fmt.Println(ctx.Value(key),"監控退出,中止了...") return default: //取出值 fmt.Println(ctx.Value(key),"goroutine監控中...") time.Sleep(2 * time.Second) } } }
咱們可使用context.WithValue
方法附加一對K-V的鍵值對,這裏Key必須是等價性的,也就是具備可比性;Value值要是線程安全的。在前面的例子,咱們經過傳遞參數的方式,把name
的值傳遞給監控函數。在這個例子裏,咱們實現同樣的效果,可是經過的是Context的Value的方式。
這樣咱們就生成了一個新的Context,這個新的Context帶有這個鍵值對,在使用的時候,能夠經過Value
方法讀取ctx.Value(key)
。
記住,使用WithValue傳值,通常是必須的值,不要什麼值都傳遞。