Go語言併發程式設計:上下文Context
context.Context型別是在 Go 1.7 版本引入到標準庫的,上下文Context主要用來在goroutine之間傳遞截止日期、停止訊號等上下文資訊,並且它是併發安全的,可以控制多個goroutine,因此它可以很方便的用於併發控制和超時控制,標準庫中的一些程式碼包也引入了Context引數,比如os/exec包、net包、database/sql包,等等。下面來介紹Context型別的使用方法。
目錄Context介紹
Context型別的應用還是比較廣的,比如http後臺服務,多個客戶端或者請求會導致啟動多個goroutine來提供服務,通過Context,我們可以很方便的實現請求資料的共享,比如token值,超時時間等,可以讓系統避免額外的資源消耗。
Context型別
Context型別是一個介面型別,定義了4個方法:
type Context interface {
Deadline() (deadline time.Time, ok bool)
Done() <-chan struct{}
Err() error
Value(key interface{}) interface{}
}
Deadline()
:獲取設定的截止日期,到截止日期時,Context會自動發起取消請求,ok為false表示沒有設定截止日期;Done()
:返回一個只讀通道chan,在當前工作完成、超時或者context被取消後關閉;Err()
:返回Context結束原因,取消時返回Canceled
錯誤,超時返回DeadlineExceeded
錯誤。Value
:獲取 key 對應的 value值,可以用它來傳遞額外的資訊和訊號。
Context 衍生
Context值是可以繁衍的,也就是可以通過一個Context值產生任意個子值,這些子值攜帶了父值的屬性和資料,也可以響應通過其父值傳達的訊號。
Context根節點是一個已經在context包中預定義好的Context值,是全域性唯一的,它既不可以被撤銷,也不能攜帶任何資料,可以通過呼叫context.Background函式獲取到它。
context包提供了4個用於繁衍Context值的函式:
WithCancel
:基於parent context 產生一個可撤銷(cancel)的子contextWithDeadline
:產生可以定時撤銷的子context,達到截止日期後,context會收到cancel通知。WithTimeout
:與WithDeadline
類似,產生可以定時撤銷的子contextWithValue
:產生攜帶額外資料的子context
下面介紹這4個函式的使用示例。
WithCancel
WithCancel返回兩個結果值,第一個是可撤銷的Context值,第二個則是用於觸發撤銷訊號的函式。在撤銷函式被執行後,先關閉內部的接收通道,然後向所有子Context傳送cancel訊號,最終斷開與父Context之間的關聯。其中cancel訊號的傳遞採用的是深度優先搜尋演算法。
仍然是取錢的例子,要求是賬戶的錢大於10000後停止存錢:
package main
import (
"context"
"fmt"
"math/rand"
"sync/atomic"
"time"
)
var (
balance int32
)
// 存錢
func deposit(value int32, id int, deferFunc func()) {
defer func() {
deferFunc()
}()
for {
currBalance := atomic.LoadInt32(&balance)
newBalance := currBalance + value
time.Sleep(time.Millisecond * 500)
if atomic.CompareAndSwapInt32(&balance, currBalance, newBalance) {
fmt.Printf("ID: %d, 存 %d 後的餘額: %d\n", id, value, balance)
break
} else {
// fmt.Printf("操作失敗\n")
}
}
}
// 取錢
func withdraw(value int32) {
for {
currBalance := atomic.LoadInt32(&balance)
newBalance := currBalance - value
if atomic.CompareAndSwapInt32(&balance, currBalance, newBalance) {
fmt.Printf("取 %d 後的餘額: %d\n", value, balance)
break
}
}
}
func WithCancelDemo() {
total := 10000
ctx, cancelFunc := context.WithCancel(context.Background())
for i := 1; i <= 100; i++ {
num := rand.Intn(2000) // 隨機數
go deposit(int32(num), i, func() {
if atomic.LoadInt32(&balance) >= int32(total) {
cancelFunc()
}
})
}
<-ctx.Done()
withdraw(10000)
fmt.Println("退出")
}
func main() {
WithCancelDemo()
}
func init() {
balance = 1000 // 初始賬戶餘額為1000
}
執行結果:
ID: 95, 存 1940 後的餘額: 2940
ID: 19, 存 1237 後的餘額: 4177
ID: 78, 存 1463 後的餘額: 5640
ID: 17, 存 1211 後的餘額: 6851
ID: 80, 存 420 後的餘額: 7271
ID: 28, 存 888 後的餘額: 8159
ID: 32, 存 408 後的餘額: 8567
ID: 50, 存 1353 後的餘額: 9920
ID: 38, 存 631 後的餘額: 10551
取 10000 後的餘額: 551
退出
WithDeadline
設定截止日期,達到截止日期後停止存錢:
func DeadlineDemo() {
total := 10000
deadline := time.Now().Add(2 * time.Second)
ctx, cancelFunc := context.WithDeadline(context.Background(), deadline)
for i := 1; i <= 100; i++ {
num := rand.Intn(2000) // 隨機數
go deposit(int32(num), i, func() {
if atomic.LoadInt32(&balance) >= int32(total) {
cancelFunc()
}
})
}
select {
case <-ctx.Done():
fmt.Println(ctx.Err())
}
fmt.Println("超時退出")
}
截止日期引數deadline是一個時間物件:time.Time
執行結果:
ID: 7, 存 1410 後的餘額: 1961
ID: 5, 存 81 後的餘額: 2042
ID: 69, 存 783 後的餘額: 2825
context deadline exceeded
超時退出
WithDeadline和WithTimeout函式生成的Context值也是可撤銷的,可以實現自動定時撤銷,也可以在截止時間達到之前進行手動撤銷(程式碼中的cancelFunc()操作)。
WithTimeout
和WithDeadline不同之處在於,時間引數為持續時間:time.Duration:
func WithTimeoutDemo() {
total := 10000
ctx, cancelFunc := context.WithTimeout(context.Background(), 2*time.Second)
for i := 1; i <= 100; i++ {
num := rand.Intn(2000) // 隨機數
go deposit(int32(num), i, func() {
if atomic.LoadInt32(&balance) >= int32(total) {
cancelFunc()
}
})
}
select {
case <-ctx.Done():
fmt.Println(ctx.Err())
}
fmt.Println("超時退出")
}
執行結果:
ID: 36, 存 1356 後的餘額: 4181
ID: 100, 存 1598 後的餘額: 5779
ID: 25, 存 47 後的餘額: 5826
ID: 10, 存 292 後的餘額: 6118
context deadline exceeded
超時退出
WithValue
WithValue函式產生的Context可以攜帶資料,和另外3種函式不同,它是不可撤銷的。Value方法用來獲取資料,沒有提供改變資料的方法。
WithValue函式產生的Context攜帶的值可以在子Context中傳遞。
func WithValueDemo() {
rootNode := context.Background()
ctx1, cancelFunc := context.WithCancel(rootNode)
defer cancelFunc()
ctx2 := context.WithValue(ctx1, "key2", "value2")
ctx3 := context.WithValue(ctx2, "key3", "value3")
fmt.Printf("ctx3: key2 %v\n", ctx3.Value("key2"))
fmt.Printf("ctx3: key3 %v\n", ctx3.Value("key3"))
fmt.Println()
ctx4, _ := context.WithTimeout(ctx3, time.Hour)
fmt.Printf("ctx4: key2 %v\n", ctx4.Value("key2"))
fmt.Printf("ctx4: key3 %v\n", ctx4.Value("key3"))
}
執行結果:
ctx3: key2 value2
ctx3: key3 value3
ctx4: key2 value2
ctx4: key3 value3
總結
Context型別是一個可以實現多 goroutine 併發控制的同步工具。Context型別主要分為三種,即:根Context、可撤銷的Context和攜帶資料的Context。根Context和衍生的Context構成一顆Context樹。需要注意的是,攜帶資料的Context不能被撤銷,可撤銷的Context無法攜帶資料。
Context比sync.WaitGroup更加靈活,在使用WaitGroup時,我們需要確定執行子任務的 goroutine 數量,如果不知道這個數量,使用WaitGroup就有風險了,採用Context就很容易解決了。