context
爲什麼有context
-
首先,如果我們在併發程序中,如果需要我們去通知子協程結束我們會怎麼做?
-
我們可能會通過一個channel+select去通知,如下:
package main import ( "fmt" "time" ) func main() { exitChan := make(chan bool) go func() { for { fmt.Println("Doing Work......") select { case <-exitChan: return } } }() time.Sleep(time.Second * 1) fmt.Println("stop the goRoutinue") close(exitChan) time.Sleep(time.Second * 3) }
-
這種做法能傳遞我們需要給子協程的信號,但是他是有限的,比如如果我想在指定的時間間隔內通知,想傳遞爲什麼取消的信息,如果需要控制子協程以及子協程的子協程,多個層級下使用exit Channel的方式會變得混亂複雜,所以官方就爲goroutine控制開發了context包
什麼是context
-
context是協程併發安全的
-
context包可以從已有的context實例派生新的context,形成context的樹狀結構,只要一個context取消了,派生出來的context將都會被取消
創建context樹:
-
第一步創建根結點, 使用emptyCtx(int類型變量)…context.Background經常用做context樹的根結點,由接收請求的第一個routine創建,不能被取消,沒有值也沒有過期時間
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 } // emptyCtx不能存儲額外信息,沒有超時時間,不能取消,都是nil // Background returns a non-nil, empty Context. It is never canceled, has no // values, and has no deadline. It is typically used by the main function, // initialization, and tests, and as the top-level Context for incoming // requests. func Background() Context { return background } // TODO returns a non-nil, empty Context. Code should use context.TODO when // it's unclear which Context to use or it is not yet available (because the // surrounding function has not yet been extended to accept a Context // parameter). func TODO() Context { return todo }
通過註釋,可以看到context.Background返回一個非空的context,通常由主函數,初始化和測試使用,並作爲傳入請求的top-level Context (頂級上下文)。TODO也是返回一個非空的context,當不知道應該使用context時使用TODO…兩者只是使用場景不同,代碼實現都是一樣
-
創建子孫節點由四個函數
- func WithCancel(parent Context) (ctx Context, cancel CancelFunc)
// 對應cancelCtx(參考文末的structure圖) // 返回ctx和cancel函數,可以讓後代的goroutine退出,關閉對應的c.done // 創建了可取消的cancelCtx類型 =》 對應下面的cancelCtx 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} } // propagateCancel arranges for child to be canceled when parent is. func propagateCancel(parent Context, child canceler) { if parent.Done() == nil { return // parent is never canceled } // 獲取cancelCtx if p, ok := parentCancelCtx(parent); ok { p.mu.Lock() if p.err != nil { // parent has already been canceled child.cancel(false, p.err) } else { if p.children == nil { p.children = make(map[canceler]struct{}) } p.children[child] = struct{}{} } p.mu.Unlock() } else { go func() { // 監聽父節點,返回的channel如果關閉,當前的context也取消了 select { case <-parent.Done(): child.cancel(false, parent.Err()) case <-child.Done(): } }() } } // parentCancelCtx follows a chain of parent references until it finds a // *cancelCtx. This function understands how each of the concrete types in this // package represents its parent. func parentCancelCtx(parent Context) (*cancelCtx, bool) { for { switch c := parent.(type) { case *cancelCtx: return c, true case *timerCtx: return &c.cancelCtx, true case *valueCtx: parent = c.Context default: return nil, false } } }
-
func WithDeadline(parent Context, d time.Time) (Context, CancelFunc)
// 對應timerCtx // WithDeadline可以設置具體的deadline時間,當到達deadline的時候,可以讓後代的goroutine退出,關閉對應的c.done func WithDeadline(parent Context, d time.Time) (Context, CancelFunc) { if cur, ok := parent.Deadline(); ok && cur.Before(d) { // The current deadline is already sooner than the new one. return WithCancel(parent) } c := &timerCtx{ cancelCtx: newCancelCtx(parent), deadline: d, } propagateCancel(parent, c) dur := time.Until(d) if dur <= 0 { c.cancel(true, DeadlineExceeded) // deadline has already passed return c, func() { c.cancel(false, Canceled) } } c.mu.Lock() defer c.mu.Unlock() if c.err == nil { c.timer = time.AfterFunc(dur, func() { c.cancel(true, DeadlineExceeded) }) } return c, func() { c.cancel(true, Canceled) } }
-
func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc)
// 對應timeCtx // 可以設置多少時間後就關閉對應的c.done,實現的方式就是計算對應的WithDeadline func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc) { return WithDeadline(parent, time.Now().Add(timeout)) }
-
func WithValue(parent Context, key, val interface{}) Context
// 對應valueCtx // 可以在context設置一個map,拿到這個context及後代可以拿到map裏的值,也是協程安全的 func WithValue(parent Context, key, val interface{}) Context { if key == nil { panic("nil key") } if !reflectlite.TypeOf(key).Comparable() { panic("key is not comparable") } // 不是在原結構體上直接添加,重新創建一個新的valueCtx子節點,找的時候是由尾部上前查找的 return &valueCtx{parent, key, val} }
-
context的方法主要有四個
-
Deadline() (deadline time.Time, ok bool) // 返回超時時間,可以對一些操作(io等)設置超時時間
-
Done() <-chan struct{} // 返回一個channel,當context 被取消了,這個channel會被關閉,對應的routine也應該結束返回
-
Err() error // 如果Done未關閉返回nil,如果已關閉則返回非nil錯誤解釋原因,對Err連續調用將返回相同的錯誤
-
Value(key interface{}) interface{} // 可以獲取context傳遞的key對應的一些數據,也是協程安全的
-
context的用法
-
使用規則:
- 不要將 Context放入結構體,Context應該作爲第一個參數傳入,命名爲ctx。
- 即使函數允許,也不要傳入nil的 Context。如果不知道用哪種 Context,可以使用context.TODO()
- 使用context的Value相關方法,只應該用於在程序和接口中傳遞和請求相關數據,不能用它來傳遞一些可選的參數
- context是協程安全的,可以傳遞不同的goroutine
-
現在重新實現上文簡單的控制
package main import ( "context" "fmt" "time" ) func work(ctx context.Context, msg string) { for { select { case <-ctx.Done(): println(msg, "goroutinue is finish......") return default: println("goroutinue is running", time.Now().String()) time.Sleep(time.Second) } } } func main() { ctx, cancel := context.WithCancel(context.Background()) go work(ctx, "withCancel") time.Sleep(time.Second * 3) println("cancel......") cancel() time.Sleep(time.Second * 3) println("finish") }
深入context
-
cancelCtx
type cancelCtx struct { Context mu sync.Mutex // protects following fields done chan struct{} // created lazily, closed by first cancel call children map[canceler]struct{} // set to nil by the first cancel call err error // set to non-nil by the first cancel call } func (c *cancelCtx) Done() <-chan struct{} { c.mu.Lock() // 如果未初始化則初始化lazyload if c.done == nil { c.done = make(chan struct{}) } d := c.done c.mu.Unlock() return d } // cancel closes c.done, cancels each of c's children, and, if // removeFromParent is true, removes c from its parent's children. func (c *cancelCtx) cancel(removeFromParent bool, err error) { if err == nil { panic("context: internal error: missing cancel error") } c.mu.Lock() if c.err != nil { c.mu.Unlock() return // already canceled } c.err = err // 發送關閉信號 if c.done == nil { c.done = closedchan } else { close(c.done) } // 將所有派生的子節點context依次取消 for child := range c.children { // NOTE: acquiring the child's lock while holding parent's lock. child.cancel(false, err) } c.children = nil c.mu.Unlock() if removeFromParent { // 移除與父節點的聯繫 removeChild(c.Context, c) } }
-
valueCtx
type valueCtx struct { Context key, val interface{} } // stringify tries a bit to stringify v, without using fmt, since we don't // want context depending on the unicode tables. This is only used by // *valueCtx.String(). func stringify(v interface{}) string { switch s := v.(type) { case stringer: return s.String() case string: return s } return "<not Stringer>" } func (c *valueCtx) String() string { return contextName(c.Context) + ".WithValue(type " + reflectlite.TypeOf(c.key).String() + ", val " + stringify(c.val) + ")" } // 沿着context向上尋找key對應的值 =》 對應WithValue func (c *valueCtx) Value(key interface{}) interface{} { if c.key == key { return c.val } return c.Context.Value(key) }
-
timeCtx
// 就是在cancelCtx的基礎上新增timer屬性,使用定時器和deadline去實現定期取消 type timerCtx struct { cancelCtx timer *time.Timer // Under cancelCtx.mu. deadline time.Time } func (c *timerCtx) Deadline() (deadline time.Time, ok bool) { return c.deadline, true } func (c *timerCtx) String() string { return contextName(c.cancelCtx.Context) + ".WithDeadline(" + c.deadline.String() + " [" + time.Until(c.deadline).String() + "])" } func (c *timerCtx) cancel(removeFromParent bool, err error) { // 取消內部的cancelCtx c.cancelCtx.cancel(false, err) if removeFromParent { // Remove this timerCtx from its parent cancelCtx's children. // 與祖先結點關係取消 removeChild(c.cancelCtx.Context, c) } c.mu.Lock() if c.timer != nil { // 取消了定時器 c.timer.Stop() c.timer = nil } c.mu.Unlock() } // 如果父節點由過期時間,子節點context可以不用設置過期時間
-
structure圖