Go語言之Context
控制併發有兩種經典的方式,一種是WaitGroup,另外一種就是Context,今天我就談談Context。
什麼是WaitGroup
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協同做一件事情的時候,因為每個goroutine做的都是這件事情的一部分,只有全部的goroutine都完成,這件事情才算是完成,這是等待的方式。
在實際的業務種,我們可能會有這麼一種場景:需要我們主動的通知某一個goroutine結束。比如我們開啟一個後臺goroutine一直做事情,比如監控,現在不需要了,就需要通知這個監控goroutine結束,不然它會一直跑,就洩漏了。
chan通知
我們都知道一個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)
}
例子中我們定義一個stop的chan,通知他結束後臺goroutine。實現也非常簡單,在後臺goroutine中,使用select判斷stop是否可以接收到值,如果可以接收到,就表示可以退出停止了;如果沒有接收到,就會執行default裡的監控邏輯,繼續監控,只到收到stop的通知。
有了以上的邏輯,我們就可以在其他goroutine種,給stop chan傳送值了,例子中是在maingoroutine中傳送的,控制讓這個監控的goroutine結束。
傳送了stop<- true結束的指令後,我這裡使用time.Sleep(5 * time.Second)故意停頓 5 秒來檢測我們結束監控goroutine是否成功。如果成功的話,不會再有goroutine監控中...的輸出了;如果沒有成功,監控goroutine就會繼續列印goroutine監控中...輸出。
這種chan+select的方式,是比較優雅的結束一個goroutine的方式,不過這種方式也有侷限性,如果有很多goroutine都需要控制結束怎麼辦呢?如果這些goroutine又衍生了其他更多的goroutine怎麼辦呢?如果一層層的無窮盡的goroutine呢?這就非常複雜了,即使我們定義很多chan也很難解決這個問題,因為goroutine的關係鏈就導致了這種場景非常複雜。
初識Context
上面說的這種場景是存在的,比如一個網路請求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)
}
重寫比較簡單,就是把原來的chan stop換成Context,使用Context跟蹤goroutine,以便進行控制,比如結束等。
context.Background()返回一個空的Context,這個空的Context一般用於整個Context樹的根節點。然後我們使用context.WithCancel(parent)函式,建立一個可取消的子Context,然後當作引數傳給goroutine使用,這樣就可以使用這個子Context跟蹤這個goroutine。
在goroutine中,使用select呼叫<-ctx.Done()判斷是否要結束,如果接受到值的話,就可以返回結束goroutine了;如果接收不到,就會繼續進行監控。
那麼是如何傳送結束指令的呢?這就是示例中的cancel函式啦,它是我們呼叫context.WithCancel(parent)函式生成子Context的時候返回的,第二個返回值就是這個取消函式,它是CancelFunc型別的。我們呼叫它就可以發出取消指令,然後我們的監控goroutine就會收到訊號,就會返回結束。
Context控制多個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)
}
}
}
示例中啟動了 3 個監控goroutine進行不斷的監控,每一個都使用了Context進行跟蹤,當我們使用cancel函式通知取消時,這 3 個goroutine都會被結束。這就是Context的控制能力,它就像一個控制器一樣,按下開關後,所有基於這個Context或者衍生的子Context都會收到通知,這時就可以進行清理操作了,最終釋放goroutine,這就優雅的解決了goroutine啟動後不可控的問題。
Context介面
Context的介面定義的比較簡潔,我們看下這個介面的方法。
type Contextinterface {
Deadline() (deadline time.Time, ok bool)
Done() <-chan struct{}
Err() error
Valu (key interface{}) interface{}
}
這個介面共有 4 個方法,瞭解這些方法的意思非常重要,這樣我們才可以更好的使用他們。
Deadline方法是獲取設定的截止時間的意思,第一個返回式是截止時間,到了這個時間點,Context會自動發起取消請求;第二個返回值ok==false時表示沒有設定截止時間,如果需要取消的話,需要呼叫取消函式進行取消。
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
}
一個是Background,主要用於main函式、初始化以及測試程式碼中,作為Context這個樹結構的最頂層的Context,也就是根Context。
一個是TODO,它目前還不知道具體的使用場景,如果我們不知道該使用什麼Context的時候,可以使用這個。
他們兩個本質上都是emptyCtx結構體型別,是一個不可取消,沒有設定截止時間,沒有攜帶任何值的Context。
type emptyCtx int
func (*emptyCtx) Deadline() (deadline time.Time, ok bool) {
return
}
func (*emptyCtx) Done() <-chan struct{} {
returnnil
}
func (*emptyCtx) Err() error {
returnnil
}
func (*emptyCtx) Value(key interface{}) interface{} {
returnnil
}
這就是emptyCtx實現Context介面的方法,可以看到,這些方法什麼都沒做,返回的都是nil或者零值。
Context的繼承衍生
有了如上的根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()
這就是取消函式的型別,該函式可以取消一個Context,以及這個節點Context下所有的所有的Context,不管有多少層級。
WithValue傳遞後設資料
透過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)
}
}
}
在前面的例子,我們透過傳遞引數的方式,把name的值傳遞給監控函式。在這個例子裡,我們實現一樣的效果,但是透過的是Context的Value的方式。
我們可以使用context.WithValue方法附加一對K-V的鍵值對,這裡Key必須是等價性的,也就是具有可比性;Value值要是執行緒安全的。
這樣我們就生成了一個新的Context,這個新的Context帶有這個鍵值對,在使用的時候,可以透過Value方法讀取ctx.Value(key)。
記住,使用WithValue傳值,一般是必須的值,不要什麼值都傳遞。
Context 使用原則
· 不要把Context放在結構體中,要以引數的方式傳遞。
· 以Context作為引數的函式方法,應該把Context作為第一個引數,放在第一位。
· 給一個函式方法傳遞Context的時候,不要傳遞nil,如果不知道傳遞什麼,就使用context.TODO。
· Context的Value相關方法應該傳遞必須的資料,不要什麼資料都使用這個傳遞。
· Context是縣城安全的,可以放心的在多個goroutine中傳遞。
來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/755/viewspace-2817149/,如需轉載,請註明出處,否則將追究法律責任。
相關文章
- 深度解密Go語言之context解密GoContext
- 深度解密 Go 語言之 context解密GoContext
- Go語言之介面Go
- Go語言之methodGo
- Go語言之 Struct TagGoStruct
- go語言之反射-------ReflectionGo反射
- Go語言之包(package)管理GoPackage
- 深度解密 Go 語言之 channel解密Go
- 深度解密Go語言之channel解密Go
- 深度解密Go語言之 map解密Go
- 深度解密GO語言之反射解密Go反射
- Go語言之讀寫鎖Go
- 深度解密Go語言之Slice解密Go
- Go語言之錯誤處理Go
- Go語言之併發示例(Runner)Go
- go語言之陣列與切片Go陣列
- Go語言之旅:基本型別Go型別
- Go 語言 context 包實踐GoContext
- 深度解密 Go 語言之 sync.map解密Go
- 深度解密 Go 語言之 sync.Pool解密Go
- go語言之結構體和方法Go結構體
- Go:context.ContextGoContext
- Go語言之切片(slice)快速入門篇Go
- Go語言之陣列快速入門篇Go陣列
- 在 Fefora 上開啟 Go 語言之旅Go
- python和GO語言之間的區別!PythonGo
- Go語言之變數逃逸(Escape Analysis)分析Go變數
- Go ContextGoContext
- Go語言Context包原始碼學習GoContext原始碼
- Go 語言基礎之 Context 詳解GoContext
- Go語言之對映(map)快速入門篇Go
- Go語言之Goroutine與通道、異常處理Go
- 深入理解GO語言之併發機制Go
- Go:context包GoContext
- go 上下文:context.ContextGoContext
- Python和GO語言之間的區別是什麼?PythonGo
- Go context 介紹GoContext
- Go語言的context包從放棄到入門GoContext