Go36-32-context.Context
context.Context
sync.WaitGroup型別是一個實現一對多goroutine協作流程的同步工具。還有另一種工具也可以實現這種協作流程。
回顧sync.WaitGroup實現協作流程
在使用WaitGroup的時候,建議是用“先統一Add,再併發Done,最後Wait”的模式來構建協作流程。要避免併發的呼叫Add方法。這就帶來一個問題,需要在一開始就能確定執行子任務的goroutine的數量,至少也是在啟動goroutine之前。
下面是一個示例,稍微做了一些改造:
package main import ( "time" "fmt" "sync" "sync/atomic" ) func coordinateWithWaitGroup() { total := 12 var num int32 var wg sync.WaitGroup // 定義好goroutine中返回前要執行的defer函式 deferFunc := func() { wg.Done() } for i := 0; i < total; i++ { wg.Add(1) go addNum(&num, i, deferFunc) } wg.Wait() } // 這個函式的defer函式通過引數來給出 func addNum(numP *int32, id int, deferFunc func()) { defer deferFunc() for i := 1; ; i++ { currNum := atomic.LoadInt32(numP) newNum := currNum + 1 time.Sleep(time.Millisecond * 200) if atomic.CompareAndSwapInt32(numP, currNum, newNum) { fmt.Printf("id: %02d 第 %02d 次更新num成功: %d\n", id, i, newNum) break } } } func main() { coordinateWithWaitGroup() }
這裡的改造是為了更像之後要使用context包時的用法,不過在使用規則上還是滿足WaitGroup的要求的。
通過context包實現協作流程
這裡就是要在寫一個coordinateWithWaitContext函式,來代替上面的coordinateWithWaitGroup函式。兩個函式要具有相同的功能。
這裡先直接給出示例程式碼了:
func coordinateWithWaitContext() { total := 12 var num int32 cxt, cancelFunc := context.WithCancel(context.Background()) // 定義好goroutine中返回前要執行的defer函式,這裡用到了上面的cancelFunc deferFunc := func() { if atomic.LoadInt32(&num) == int32(total) { cancelFunc() } } for i := 0; i < total; i++ { go addNum(&num, i, deferFunc) } <- cxt.Done() }
所有的變化都在上面這個函式裡了。這裡先後呼叫了context.Background函式和context.WithCancel函式。得到了一個可撤銷context.Context型別的值,賦值給了變數cxt。還有一個context.CancelFunc型別的撤銷函式,賦值給了變數cancelFunc。
這裡在判斷goroutine執行完畢的依據是通過判斷num裡的值。一旦判斷完成,就會呼叫之前準備好的cancelFunc函式,此時cxt.Done函式返回的通道就會接收到值,結束等待。
和WaitGroup的比較
WaitGroup需要事先知道所有goroutine的數量,而context這裡更關心是否滿足某個條件,一旦條件滿足就可以退出。
這裡我想提一下python,讓我想到了python中的for迴圈和while迴圈。能用for迴圈就不要用while迴圈。使用while迴圈可能由於條件判斷複雜了,造成條件永遠無法滿足而成了死迴圈。使用for迴圈的話就沒有這個問題了。不過當迴圈的退出和數量沒有關係時,只能用while迴圈了。
就好比WaitGroup,如果可以通過goroutine的數量判斷,那麼應該還是使用WaitGroup好。如果遇到結束條件和goroutine數量無關的時候,就只能用context了。
context.Context型別
context.Context型別,是在Go 1.7釋出時才被加入到標準庫的。而後,標準庫中的很多其他程式碼包都為了支援它而進行了擴充套件,包括:os/exec包、net包、database/sql包、runtime/pprof包和runtime/trace包,等等。
之所以會收到眾多程式碼包的積極支援,主要因為它是一種非常通用的同步工具。它的值不但可以任意的擴散,而且還可以被用來傳遞額外的資訊和訊號。就是Context型別可以提供一個代表上下文的值,之類值是併發安全的,也就是說它可以被傳播給多個goroutine。
介面型別Context最新實際是一個介面型別,在context包中實現該介面的所有私有型別,都是基於某個資料型別的指標型別。所以,如此傳播並不會影響該型別值的功能和安全。
可繁衍的
Context型別的值是可以繁衍的,這意味著可以通過一個Context值產生出任意個子值。這些子值可以攜帶父值的屬性和資料,也可以相應通過其父值傳達的訊號。如此,所有的Context值共同構成了一顆代表了上下文全貌的屬性結構。樹的根節點是一個已經在context包中預定義好的context值,它是全域性唯一的。通過呼叫context.Background函式,就可以獲取到它。
包內的函式
在context包中,包含了4個用於繁衍Context值的函式:
- WithCancel,產生一個可撤銷的parent的子值
- WithDeadline,產生一個會定時撤銷的parent的子值
- WithTimeout,同上,也是定時撤銷的parent的子值
- WithValue,產生一個會攜帶額外資料的parent的子值
這些函式的第一個引數型別都是context.Context,而名稱都為parent。顧名思義,這個位置上的引數對應的都是產生Context值的父值。
撤銷訊號在上下文樹中的傳播
context包中的WithCancel、WithDeadline和WithTimeout都是被用來基於給定的COntext值產生可撤銷的子值的。
這個函式在被呼叫後,產生兩個結果值。第一個是可撤銷的Context值,第二個是用於觸發撤銷訊號的函式。
撤銷函式被呼叫後,對應的Context值會先關閉它內部的接收通道,通道關閉了接收該通道的操作就會立即返回,就是Done方法返回的那個通道。然後,它還會向它的所有子值傳達撤銷訊號。這些子值如果還有子值,就會一級一級把撤銷訊號傳遞下去。最後,這個Context值會斷開它與其父值之間的關聯。
通過呼叫WithDeadline函式或者WithTimeout函式生成的Context值也是可撤銷的。它們不但可以被手動撤銷,還會依據在生成是給定的過期時間,自動地進行定時撤銷。這裡的定時撤銷功能是藉助它們內部的計時器來實現的。
當過期時間到達時,兩種Context值的行為與手動撤銷是的行為是幾乎一致的,只是多了一步停止並釋放掉內部的計時器。
WithDeadline和WithTimeout是相似的。都是通過設定,會在某個時間自動觸發,就是ctx.Done()能夠取到值。差別是,DeadLine是設定一個時間點,時間對上了就到期。Timeout是設定一段時間,比如幾秒,過個這段時間,就超時。其實底層的Timeout也是通過Deadlin實現的。
WithValue這個函式得到的值是不可撤銷的。撤銷訊號在傳播時,若遇到它們會直接跨過,並試圖將資訊直接傳給它們的子值。
傳遞資料
通過WithValue函式產生新的Context值的時候需要3個引數:父值、鍵和值。這裡鍵必須是可判斷等的,類似字典的鍵。不過Context值並不是用字典來儲存鍵和值的,而是簡單地儲存在父值相應的欄位中。
通過Value方法,可以獲取資料。在呼叫包含屬性的Context值的Value方法是,會先判斷給定的鍵,如有有就返回儲存的值,否則會到其父值中繼續查詢,會一直沿著上下文根節點的方法一直查詢。因為其他幾種Context值都是無法攜帶資料的,所以Value方法在查詢的時候,會跨過這這些Context值。
無法改變資料
Context介面沒有提供改變資料的方法,所以通常只能通過在上下文數中新增含資料的Context值來儲存新的資料,或者通過撤銷此種值的父值丟棄掉相應的資料。如果儲存在這裡的資料可以從外部改變,那麼必須自信保證安全。
下面這個示例展示了Context值裡資料的傳遞:
package main import ( "context" "fmt" "time" ) type myKey int func main() { keys := []myKey{ myKey(20), myKey(30), myKey(60), myKey(61), } values := []string{ "value in node2", "value in node3", "value in node6", "value in node6Branch", } rootNode := context.Background() node1, cancelFunc1 := context.WithCancel(rootNode) defer cancelFunc1() node2 := context.WithValue(node1, keys[0], values[0]) node3 := context.WithValue(node2, keys[1], values[1]) fmt.Printf("The value of the key %v found in the node3: %v\n", keys[0], node3.Value(keys[0])) fmt.Printf("The value of the key %v found in the node3: %v\n", keys[1], node3.Value(keys[1])) fmt.Printf("The value of the key %v found in the node3: %v\n", keys[2], node3.Value(keys[2])) fmt.Println() node4, cancelFunc4 := context.WithCancel(node3) defer cancelFunc4() node5, cancelFunc5 := context.WithTimeout(node4, time.Hour) defer cancelFunc5() fmt.Printf("The value of the key %v found in the node5: %v\n", keys[0], node5.Value(keys[0])) fmt.Printf("The value of the key %v found in the node5: %v\n", keys[1], node5.Value(keys[1])) fmt.Println() node6 := context.WithValue(node5, keys[2], values[2]) fmt.Printf("The value of the key %v found in the node6: %v\n", keys[0], node6.Value(keys[0])) fmt.Printf("The value of the key %v found in the node6: %v\n", keys[2], node6.Value(keys[2])) fmt.Println() node6Branch := context.WithValue(node5, keys[3], values[3]) fmt.Printf("The value of the key %v found in the node6Branch: %v\n", keys[1], node6Branch.Value(keys[1])) fmt.Printf("The value of the key %v found in the node6Branch: %v\n", keys[2], node6Branch.Value(keys[2])) fmt.Printf("The value of the key %v found in the node6Branch: %v\n", keys[3], node6Branch.Value(keys[3])) fmt.Println() node7, cancelFunc7 := context.WithCancel(node6) defer cancelFunc7() node8, cancelFunc8 := context.WithTimeout(node7, time.Hour) defer cancelFunc8() fmt.Printf("The value of the key %v found in the node8: %v\n", keys[1], node8.Value(keys[1])) fmt.Printf("The value of the key %v found in the node8: %v\n", keys[2], node8.Value(keys[2])) fmt.Printf("The value of the key %v found in the node8: %v\n", keys[3], node8.Value(keys[3])) }
總結
Context型別是一個可以實現多goroutine協作流程同步的工具。還可以通過它的值傳達撤銷訊號或傳遞資料。
Context型別的值大體可分3種:
- 根Context值
- 可撤銷的Context值
- 含資料的Context值
所有的Context值共同構成了一顆上下文樹。這棵樹的作用域是全域性的,根Context值就是樹的根,它也是全域性唯一的,並且不提供任何額外的功能。
包含資料的Context值不能被撤銷,可撤銷的Context值又無法攜帶資料。但是,由於它們共同組成了一個有機的整體,即上下文數,所以在功能上要比sync.WaitGroup強大的多。
這個系列偏重理論,就少了很多實際的應用,關於context包,我之前還有一篇:
http://blog.51cto.com/steed/2330218 在這篇裡介紹了兩個主要功能:
- 控制超時時間
- 儲存上下文資料