簡介
- 在 Go http包的Server中,每一個請求在都有一個對應的 goroutine 去處理。請求處理函數通常會啓動額外的 goroutine 用來訪問後端服務,比如數據庫和RPC服務。用來處理一個請求的 goroutine 通常需要訪問一些與請求特定的數據,比如終端用戶的身份認證信息、驗證相關的token、請求的截止時間。 當一個請求被取消或超時時,所有用來處理該請求的 goroutine 都應該迅速退出,然後系統才能釋放這些 goroutine 佔用的資源。
- 在Google 內部,我們開發了 Context 包,專門用來簡化 對於處理單個請求的多個 goroutine 之間與請求域的數據、取消信號、截止時間等相關操作,這些操作可能涉及多個 API 調用。現在context已成爲官方庫,使用的時候只需要
import "context"
即可。
Context基本數據結構
【Context interface】
Context interface是最基本的接口:
type Context interface {
Deadline() (deadline time.Time, ok bool)
Done() <-chan struct{}
Err() error
Value(key interface{}) interface{}
}
- Deadline()返回一個time.Time,是當前 Context 的應該結束的時間,ok 表示是否有 deadline
- Done()返回一個 channel,這個 channel 對於以 Context 方式運行的函數而言,是一個取消信號。當這個 channel 關閉時,上面提到的這些函數應該終止手頭的工作並立即返回。
- Err()返回 Context 被取消時的錯誤
- Value(key interface{}) 允許 Context 對象攜帶request作用域的數據,該數據必須是線程安全的。
【canceler interface】
canceler interface 定義了提供 cancel 函數的 context,要求數據結構要同時實現 Context interface。
type canceler interface {
cancel(removeFromParent bool, err error)
Done() <-chan struct{}
}
【Structs】
除了以上兩個 interface 之外,context 包中還定義了若干個struct,來實現上面的 interface
- emptyCtx
emptyCtx是空的Context,只實現了Context interface,只能作爲 root context 使用。
type emptyCtx int
- cancelCtx
cancelCtx繼承了Context並實現了cancelerinterface,從WithCancel()函數產生.
type cancelCtx struct {
Context
done chan struct{} // closed by the first cancel call.
mu sync.Mutex
children map[canceler]bool // set to nil by the first cancel call
err error // set to non-nil by the first cancel call
}
- timerCtx
timerCtx繼承了cancelCtx,所以也自然實現了Context和canceler這兩個interface,由WithDeadline()函數產生。·
type timerCtx struct {
cancelCtx
timer *time.Timer // Under cancelCtx.mu.
deadline time.Time
}
- valueCtx
valueCtx包含key、val field,可以儲存一對鍵值對,由WithValue()函數產生。
type valueCtx struct {
Context
key, val interface{}
}
Context實例化和派生
【Background()函數】
Context 只定義了 interface,真正使用時需要實例化,官方首先定義了一個 emptyCtx struct 來實現 Context interface,然後提供了Backgroud()函數來便利的生成一個 emptyCtx 實例。實現代碼如下:
// An emptyCtx is never canceled, has no values, and has no deadline. It is not
// struct{}, since vars of this type must have distinct addresses.
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
}
func (e *emptyCtx) String() string {
switch e {
case background:
return "context.Background"
case todo:
return "context.TODO"
}
return "unknown empty Context"
}
var (
background = new(emptyCtx)
todo = new(emptyCtx)
)
func Background() Context {
return background
}
Backgroud() 生成的 emptyCtx 實例是不能取消的,因爲emptyCtx沒有實現canceler interface,要正常取消功能的話,還需要對 emptyCtx 實例進行派生。常見的兩種派生用法是WithCancel() 和 WithTimeout。
【WithCancel()函數】
調用WithCancel()可以將基礎的 Context 進行繼承,返回一個cancelCtx示例,並返回一個函數,可以在外層直接調用cancelCtx.cancel()來取消 Context。代碼如下:
func WithCancel(parent Context) (ctx Context, cancel CancelFunc) {
c := newCancelCtx(parent)
propagateCancel(parent, &c)
return &c, func() { c.cancel(true, Canceled) }
}
// newCancelCtx returns an initialized cancelCtx.
func newCancelCtx(parent Context) cancelCtx {
return cancelCtx{
Context: parent,
done: make(chan struct{}),
}
}
【WithTimeout()函數】
調用WithTimeout,需要傳一個超時時間。來指定過多長時間後超時結束 Context,源代碼中可以得知WithTimeout是WithDeadline的一層皮,WithDeadline傳的是具體的結束時間點,這個在工程中並不實用,WithTimeout會根據運行時的時間做轉換。代碼如下:
func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc) {
return WithDeadline(parent, time.Now().Add(timeout))
func WithDeadline(parent Context, deadline time.Time) (Context, CancelFunc) {
if cur, ok := parent.Deadline(); ok && cur.Before(deadline) {
// The current deadline is already sooner than the new one.
return WithCancel(parent)
}
c := &timerCtx{
cancelCtx: newCancelCtx(parent),
deadline: deadline,
}
propagateCancel(parent, c)
d := deadline.Sub(time.Now())
if d <= 0 {
c.cancel(true, DeadlineExceeded) // deadline has already passed
return c, func() { c.cancel(true, Canceled) }
}
c.mu.Lock()
defer c.mu.Unlock()
if c.err == nil {
c.timer = time.AfterFunc(d, func() {
c.cancel(true, DeadlineExceeded)
})
}
return c, func() { c.cancel(true, Canceled) }
}
在WithDeadline中,將 timeCtx.timer 掛上結束時的回調函數,回調函數的內容是調用cancel來結束 Context。
【WithValue()函數】
創建一個存儲 k-v 對的 context。代碼如下:
func WithValue(parent Context, key, val interface{}) Context {
if key == nil {
panic("nil key")
}
if !reflect.TypeOf(key).Comparable() {
panic("key is not comparable")
}
return &valueCtx{parent, key, val}
}
Context控制多個goroutine示例
以下代碼啓動了 3 個監控 goroutine 進行不斷的運行任務,每一個都使用了 Context 進行跟蹤,當我們使用 cancel 函數通知取消時,這 3 個 goroutine 都會被結束。這就是 Context 的控制能力,它就像一個控制器一樣,按下開關後,所有基於這個 Context 或者衍生的子 Context 都會收到通知,這時就可以進行清理操作了,最終釋放 goroutine,這就解決了 goroutine 啓動後不可控的問題。
package main
import (
"fmt"
"time"
"context"
)
// 使用context控制多個goroutine
func watch(ctx context.Context, name string) {
for {
select {
case <- ctx.Done():
fmt.Println(name, "退出 ,停止了。。。")
return
default:
fmt.Println(name, "運行中。。。")
time.Sleep(2 * time.Second)
}
}
}
func main() {
ctx, cancel := context.WithCancel(context.Background())
go watch(ctx, "【任務1】")
go watch(ctx, "【任務2】")
go watch(ctx, "【任務3】")
time.Sleep(time.Second * 10)
fmt.Println("通知任務停止。。。。")
cancel()
time.Sleep(time.Second * 5)
fmt.Println("真的停止了。。。")
}
運行結果如下:
參考博客
https://studygolang.com/articles/9624
https://www.cnblogs.com/qcrao-2018/archive/2019/06/12/11007503.html