Skip to content

context

context譯名為上下文,其設計的初衷是用來跨多個協程尤其是父子協程間傳遞信號和一些簡單的數據。它通常在處理 HTTP 請求、任務調度、數據庫查詢等場景中使用,尤其是在微服務架構中,gRPC 就通過context來跨進程跨網絡來做元數據傳遞,鏈路控制等操作。

go
package main

import (
    "context"
    "fmt"
    "os/signal"
    "syscall"
    "time"
)

func main() {
    ctx, stop := signal.NotifyContext(context.Background(), syscall.SIGKILL, syscall.SIGINT, syscall.SIGTERM)
    defer stop()
    for {
       select {
       case <-ctx.Done():
          fmt.Println("terminate")
          return
       default:
       }
       fmt.Println("running")
       time.Sleep(100 * time.Millisecond)
    }
}

上面這個案例中,通過context來傳遞進程信號,當接收到信號後,程序就會自行退出,這也是context的一個應用場景。

結構

context.context並不是一個具體的實現,而是一個定義了一組方法的接口

go
// Context's methods may be called by multiple goroutines simultaneously.
type Context interface {
	Deadline() (deadline time.Time, ok bool)

	Done() <-chan struct{}

	Err() error

	Value(key any) any
}
  • Deadline(),返回一個截至日期,和布爾值表示是否已經結束
  • Done(),返回一個管道,用於通知結束消息
  • Err(),返回上下文關閉的原因
  • Value(),根據 key 獲取指定的值

標准庫中總共有下面幾個可用的上下文

  • Background,空白的上下文,通常用於上下文的根節點
  • WithCancelWithCancelCause,可取消的上下文
  • WithDeadlineWithDeadlineCause,帶截至時間的上下文
  • WithTimeoutWithTimeoutCause,帶超時時間的上下文
  • WithValue,可以傳值的上下文

具體到實現就主要是

  • timerCtx
  • cancelCtx
  • emptyCtx

所以它的核心功能就四個點

  • 取消
  • 截止
  • 傳值
  • 傳播

弄懂了這幾個,基本上就明白了context的運作原理。

取消

go
type cancelCtx struct {
	Context

	mu       sync.Mutex            // protects following fields
	done     atomic.Value          // of 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
	cause    error                 // set to non-nil by the first cancel call
}


func withCancel(parent Context) *cancelCtx {
    if parent == nil {
       panic("cannot create context from nil parent")
    }
    c := &cancelCtx{}
    c.propagateCancel(parent, c)
    return c
}

cancelCtx的核心就在於propagateCancel這個方法,它負責將可取消這一行為傳播到父子上下文中。

go
func (c *cancelCtx) propagateCancel(parent Context, child canceler) {
	...
}
  1. 首先它會調用檢查父 context 能否被取消,不能的話就直接返回

    go
    done := parent.Done()
    if done == nil {
    	return // parent is never canceled
    }
  2. 返回檢查父 context 是否已經被取消了,是的話則取消所有的子 context

    go
    select {
    case <-done:
    	// parent is already canceled
    	child.cancel(false, parent.Err(), Cause(parent))
    	return
    default:
    }
  3. 嘗試將父 context 轉換為cancelCtx類型,如果成功了的話就會將當前 context 加入到父 context 的children中。

    go
    if p, ok := parentCancelCtx(parent); ok {
    	// parent is a *cancelCtx, or derives from one.
    	p.mu.Lock()
    	if p.err != nil {
    		// parent has already been canceled
    		child.cancel(false, p.err, p.cause)
    	} else {
    		if p.children == nil {
    			p.children = make(map[canceler]struct{})
    		}
    		p.children[child] = struct{}{}
    	}
    	p.mu.Unlock()
    	return
    }
  4. 嘗試將其轉換為afterFuncer類型,如果成功了的話,就會將取消當前 context 的方法注冊到父 context 的AfterFunc

    go
    if a, ok := parent.(afterFuncer); ok {
    	// parent implements an AfterFunc method.
    	c.mu.Lock()
    	stop := a.AfterFunc(func() {
    		child.cancel(false, parent.Err(), Cause(parent))
    	})
    	c.Context = stopCtx{
    		Context: parent,
    		stop:    stop,
    	}
    	c.mu.Unlock()
    	return
    }
  5. 如果還是不行,那麼就單獨開一個協程來監聽Done管道,當收到信號時,就會取消子 context

    go
    go func() {
        select {
        case <-parent.Done():
           child.cancel(false, parent.Err(), Cause(parent))
        case <-child.Done():
        }
    }()

然後由cancelCtx.cancel方法最終來負責取消子 context

go
func (c *cancelCtx) cancel(removeFromParent bool, err, cause error) {
	...
}

它的流程如下

  1. 檢查當前 context 是否已經被取消

    go
    if err == nil {
    	panic("context: internal error: missing cancel error")
    }
    if cause == nil {
    	cause = err
    }
    c.mu.Lock()
    if c.err != nil {
    	c.mu.Unlock()
    	return // already canceled
    }
  2. 關閉done管道,發送關閉通知

    go
    c.err = err
    c.cause = cause
    d, _ := c.done.Load().(chan struct{})
    if d == nil {
    	c.done.Store(closedchan)
    } else {
    	close(d)
    }
  3. 遍歷通知子 context

    go
    for child := range c.children {
    	child.cancel(false, err, cause)
    }
    c.children = nil
    c.mu.Unlock()
  4. 最根據傳入的參數判斷是否需要從父 context 中刪除

    if removeFromParent {
    	removeChild(c.Context, c)
    }

截止

WithTimeoutWithDeadline都是具有截至時間的上下文,兩者都是一個類型,僅僅只是使用的語義不一樣,且都基於cancelCtx

go
type timerCtx struct {
	cancelCtx
	timer *time.Timer // Under cancelCtx.mu.

	deadline time.Time
}

WithDeadlineCause負責創建具有截至時間的上下文

go
func WithDeadlineCause(parent Context, d time.Time, cause error) (Context, CancelFunc) {
	...
}

它的流程如下

  1. 檢查截至日期,如果父截止日期比當前截止日期早,那麼父 context 肯定比當前 context 先取消,則直接創建cancelCtx類型的上下文

    go
    if parent == nil {
    	panic("cannot create context from nil parent")
    }
    if cur, ok := parent.Deadline(); ok && cur.Before(d) {
    	// The current deadline is already sooner than the new one.
    	return WithCancel(parent)
    }
  2. 構建timeCtx,並傳播到子 context

    go
    c := &timerCtx{
    	deadline: d,
    }
    c.cancelCtx.propagateCancel(parent, c)
  3. 計算當前截止時間,如果已經截止了就直接取消

    go
    dur := time.Until(d)
    if dur <= 0 {
    	c.cancel(true, DeadlineExceeded, cause) // deadline has already passed
    	return c, func() { c.cancel(false, Canceled, nil) }
    }
  4. 如果沒有截止的話,就通過time.AfterFunc設置在截至時間取消當前 context

    go
    c.mu.Lock()
    defer c.mu.Unlock()
    if c.err == nil {
    	c.timer = time.AfterFunc(dur, func() {
    		c.cancel(true, DeadlineExceeded, cause)
    	})
    }
    return c, func() { c.cancel(true, Canceled, nil) }

對於timerCtx而言,它取消上下文方法就只是將timer停止,然後順便停止子 context

go
func (c *timerCtx) cancel(removeFromParent bool, err, cause error) {
	c.cancelCtx.cancel(false, err, cause)
	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()
}

傳值

valueCtx可以在上下文中傳值,獲取值

go
type valueCtx struct {
    Context
    key, val any
}

func WithValue(parent Context, key, val any) Context {
	if parent == nil {
		panic("cannot create context from nil parent")
	}
	if key == nil {
		panic("nil key")
	}
	if !reflectlite.TypeOf(key).Comparable() {
		panic("key is not comparable")
	}
	return &valueCtx{parent, key, val}
}

其核心就在於value函數,其它的 context 具體實現也會進入到該函數來取值,比如

go
func (c *valueCtx) Value(key any) any {
	if c.key == key {
		return c.val
	}
	return value(c.Context, key)
}

func (c *cancelCtx) Value(key any) any {
	if key == &cancelCtxKey {
		return c
	}
	return value(c.Context, key)
}

value函數是一個大的 for 循環,通過不斷向上遞歸來尋找指定 key 的值

go
func value(c Context, key any) any {
	for {
		switch ctx := c.(type) {
		case *valueCtx:
			if key == ctx.key {
				return ctx.val
			}
			c = ctx.Context
		case *cancelCtx:
			if key == &cancelCtxKey {
				return c
			}
			c = ctx.Context
		case withoutCancelCtx:
			if key == &cancelCtxKey {
				// This implements Cause(ctx) == nil
				// when ctx is created using WithoutCancel.
				return nil
			}
			c = ctx.c
		case *timerCtx:
			if key == &cancelCtxKey {
				return &ctx.cancelCtx
			}
			c = ctx.Context
		case backgroundCtx, todoCtx:
			return nil
		default:
			return c.Value(key)
		}
	}
}

不同 Context 類型的處理

  • *valueCtx:如果 key 匹配當前 Contextkey,則返回與 key 對應的 val
  • *cancelCtx:如果 key 匹配 cancelCtxKey,則返回當前的 cancelCtx,表示返回其自身。
  • withoutCancelCtx:表示沒有取消功能的 Context,如果 key 匹配 cancelCtxKey,則返回 nil
  • *timerCtx:如果 key 匹配 cancelCtxKey,返回與其關聯的 cancelCtx
  • backgroundCtxtodoCtx:通常是沒有攜帶任何額外值的特殊類型的 Context,遇到這兩種類型時直接返回 nil
  • 如果是未知類型,則繼續調用Value方法尋找。

小結

這幾個 context 的核心就是cancelCtx,通過它來傳播取消信號,其它的 context 類型以及一些第三方的 context 類型也都是一層套一層,在此基礎上實現各式各樣的功能。

Golang學習網由www.golangdev.cn整理維護