go協程控制——context

context

爲什麼有context

  1. 首先,如果我們在併發程序中,如果需要我們去通知子協程結束我們會怎麼做?

  2. 我們可能會通過一個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)
    }
    
  3. 這種做法能傳遞我們需要給子協程的信號,但是他是有限的,比如如果我想在指定的時間間隔內通知,想傳遞爲什麼取消的信息,如果需要控制子協程以及子協程的子協程,多個層級下使用exit Channel的方式會變得混亂複雜,所以官方就爲goroutine控制開發了context包

什麼是context

  1. context是協程併發安全的

  2. context包可以從已有的context實例派生新的context,形成context的樹狀結構,只要一個context取消了,派生出來的context將都會被取消

創建context樹:

  1. 第一步創建根結點, 使用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…兩者只是使用場景不同,代碼實現都是一樣

  2. 創建子孫節點由四個函數

    • 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}
      }
      
  3. 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的用法

  1. 使用規則:

    • 不要將 Context放入結構體,Context應該作爲第一個參數傳入,命名爲ctx。
    • 即使函數允許,也不要傳入nil的 Context。如果不知道用哪種 Context,可以使用context.TODO()
    • 使用context的Value相關方法,只應該用於在程序和接口中傳遞和請求相關數據,不能用它來傳遞一些可選的參數
    • context是協程安全的,可以傳遞不同的goroutine
  2. 現在重新實現上文簡單的控制

    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")
    }
    
    
  3. 更多運用

深入context

  1. 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)
    	}
    }
    
  2. 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)
    }
    
  3. 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可以不用設置過期時間
    
  4. structure圖
    context Structure

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章