1. 程式人生 > 其它 >Go語言併發程式設計:上下文Context

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)的子context
  • WithDeadline:產生可以定時撤銷的子context,達到截止日期後,context會收到cancel通知。
  • WithTimeout:與 WithDeadline 類似,產生可以定時撤銷的子context
  • WithValue:產生攜帶額外資料的子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就很容易解決了。

--THE END--