這篇文章將介紹Golang
併發編程中經常使用到一種編程模式:context
。本文將從爲何須要context
出發,深刻了解context
的實現原理,以及瞭解如何使用context
。golang
在併發程序中,因爲超時、取消操做或者一些異常狀況,每每須要進行搶佔操做或者中斷後續操做。熟悉channel
的朋友應該都見過使用done channel
來處理此類問題。好比如下這個例子:web
func main() {
messages := make(chan int, 10)
done := make(chan bool)
defer close(messages)
// consumer
go func() {
ticker := time.NewTicker(1 * time.Second)
for _ = range ticker.C {
select {
case <-done:
fmt.Println("child process interrupt...")
return
default:
fmt.Printf("send message: %d\n", <-messages)
}
}
}()
// producer
for i := 0; i < 10; i++ {
messages <- i
}
time.Sleep(5 * time.Second)
close(done)
time.Sleep(1 * time.Second)
fmt.Println("main process exit!")
}
複製代碼
上述例子中定義了一個buffer
爲0的channel done
, 子協程運行着定時任務。若是主協程須要在某個時刻發送消息通知子協程中斷任務退出,那麼就可讓子協程監聽這個done channel
,一旦主協程關閉done channel
,那麼子協程就能夠推出了,這樣就實現了主協程通知子協程的需求。這很好,可是這也是有限的。編程
若是咱們能夠在簡單的通知上附加傳遞額外的信息來控制取消:爲何取消,或者有一個它必需要完成的最終期限,更或者有多個取消選項,咱們須要根據額外的信息來判斷選擇執行哪一個取消選項。安全
考慮下面這種狀況:假如主協程中有多個任務1, 2, …m,主協程對這些任務有超時控制;而其中任務1又有多個子任務1, 2, …n,任務1對這些子任務也有本身的超時控制,那麼這些子任務既要感知主協程的取消信號,也須要感知任務1的取消信號。網絡
若是仍是使用done channel
的用法,咱們須要定義兩個done channel
,子任務們須要同時監聽這兩個done channel
。嗯,這樣其實好像也還行哈。可是若是層級更深,若是這些子任務還有子任務,那麼使用done channel
的方式將會變得很是繁瑣且混亂。併發
咱們須要一種優雅的方案來實現這樣一種機制:app
這個時候context
就派上用場了。咱們首先看看context
的結構設計和實現原理。函數
先看Context
接口結構,看起來很是簡單。post
type Context interface {
Deadline() (deadline time.Time, ok bool)
Done() <-chan struct{}
Err() error
Value(key interface{}) interface{}
}
複製代碼
Context
接口包含四個方法:測試
Deadline
返回綁定當前context
的任務被取消的截止時間;若是沒有設按期限,將返回ok == false
。Done
當綁定當前context
的任務被取消時,將返回一個關閉的channel
;若是當前context
不會被取消,將返回nil
。Err
若是Done
返回的channel
沒有關閉,將返回nil
;若是Done
返回的channel
已經關閉,將返回非空的值表示任務結束的緣由。若是是context
被取消,Err
將返回Canceled
;若是是context
超時,Err
將返回DeadlineExceeded
。Value
返回context
存儲的鍵值對中當前key
對應的值,若是沒有對應的key
,則返回nil
。能夠看到Done
方法返回的channel
正是用來傳遞結束信號以搶佔並中斷當前任務;Deadline
方法指示一段時間後當前goroutine
是否會被取消;以及一個Err
方法,來解釋goroutine
被取消的緣由;而Value
則用於獲取特定於當前任務樹的額外信息。而context
所包含的額外信息鍵值對是如何存儲的呢?其實能夠想象一顆樹,樹的每一個節點可能攜帶一組鍵值對,若是當前節點上沒法找到key
所對應的值,就會向上去父節點裏找,直到根節點,具體後面會說到。
再來看看context
包中的其餘關鍵內容。
emptyCtx
是一個int
類型的變量,但實現了context
的接口。emptyCtx
沒有超時時間,不能取消,也不能存儲任何額外信息,因此emptyCtx
用來做爲context
樹的根節點。
// 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
}
func TODO() Context {
return todo
}
複製代碼
但咱們通常不會直接使用emptyCtx
,而是使用由emptyCtx
實例化的兩個變量,分別能夠經過調用Background
和TODO
方法獲得,但這兩個context
在實現上是同樣的。那麼Background
和TODO
方法獲得的context
有什麼區別呢?能夠看一下官方的解釋:
// 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.
// 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).
複製代碼
Background
和TODO
只是用於不一樣場景下:Background
一般被用於主函數、初始化以及測試中,做爲一個頂層的context
,也就是說通常咱們建立的context
都是基於Background
;而TODO
是在不肯定使用什麼context
的時候纔會使用。
下面將介紹兩種不一樣功能的基礎context
類型:valueCtx
和cancelCtx
。
type valueCtx struct {
Context
key, val interface{}
}
func (c *valueCtx) Value(key interface{}) interface{} {
if c.key == key {
return c.val
}
return c.Context.Value(key)
}
複製代碼
valueCtx
利用一個Context
類型的變量來表示父節點context
,因此當前context
繼承了父context
的全部信息;valueCtx
類型還攜帶一組鍵值對,也就是說這種context
能夠攜帶額外的信息。valueCtx
實現了Value
方法,用以在context
鏈路上獲取key
對應的值,若是當前context
上不存在須要的key
,會沿着context
鏈向上尋找key
對應的值,直到根節點。
WithValue
用以向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
結構體上直接添加,而是以此context
做爲父節點,從新建立一個新的valueCtx
子節點,將鍵值對添加在子節點上,由此造成一條context
鏈。獲取value
的過程就是在這條context
鏈上由尾部上前搜尋:
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
}
type canceler interface {
cancel(removeFromParent bool, err error)
Done() <-chan struct{}
}
複製代碼
跟valueCtx
相似,cancelCtx
中也有一個context
變量做爲父節點;變量done
表示一個channel
,用來表示傳遞關閉信號;children
表示一個map
,存儲了當前context
節點下的子節點;err
用於存儲錯誤信息表示任務結束的緣由。
再來看一下cancelCtx
實現的方法:
func (c *cancelCtx) Done() <-chan struct{} {
c.mu.Lock()
if c.done == nil {
c.done = make(chan struct{})
}
d := c.done
c.mu.Unlock()
return d
}
func (c *cancelCtx) Err() error {
c.mu.Lock()
err := c.err
c.mu.Unlock()
return err
}
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
設置一個關閉的channel或者將done channel關閉,用以發送關閉信號
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 {
// 將當前context節點從父節點上移除
removeChild(c.Context, c)
}
}
複製代碼
能夠發現cancelCtx
類型變量其實也是canceler
類型,由於cancelCtx
實現了canceler
接口。Done
方法和Err
方法不必說了,cancelCtx
類型的context
在調用cancel
方法時會設置取消緣由,將done channel
設置爲一個關閉channel
或者關閉channel
,而後將子節點context
依次取消,若是有須要還會將當前節點從父節點上移除。
WithCancel
函數用來建立一個可取消的context
,即cancelCtx
類型的context
。WithCancel
返回一個context
和一個CancelFunc
,調用CancelFunc
便可觸發cancel
操做。直接看源碼:
type CancelFunc func()
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 {
// 將parent做爲父節點context生成一個新的子節點
return cancelCtx{Context: parent}
}
func propagateCancel(parent Context, child canceler) {
if parent.Done() == nil {
// parent.Done()返回nil代表父節點以上的路徑上沒有可取消的context
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{})
}
// 將當前子節點加入最近cancelCtx祖先節點的children中
p.children[child] = struct{}{}
}
p.mu.Unlock()
} else {
go func() {
select {
case <-parent.Done():
child.cancel(false, parent.Err())
case <-child.Done():
}
}()
}
}
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
}
}
}
複製代碼
以前說到cancelCtx
取消時,會將後代節點中全部的cancelCtx
都取消,propagateCancel
即用來創建當前節點與祖先節點這個取消關聯邏輯。
parent.Done()
返回nil
,代表父節點以上的路徑上沒有可取消的context
,不須要處理;context
鏈上找到到cancelCtx
類型的祖先節點,則判斷這個祖先節點是否已經取消,若是已經取消就取消當前節點;不然將當前節點加入到祖先節點的children
列表。parent.Done()
和child.Done()
,一旦parent.Done()
返回的channel
關閉,即context
鏈中某個祖先節點context
被取消,則將當前context
也取消。這裏或許有個疑問,爲何是祖先節點而不是父節點?這是由於當前context
鏈多是這樣的:
當前cancelCtx
的父節點context
並非一個可取消的context
,也就無法記錄children
。
timerCtx
是一種基於cancelCtx
的context
類型,從字面上就能看出,這是一種能夠定時取消的context
。
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) 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()
}
複製代碼
timerCtx
內部使用cancelCtx
實現取消,另外使用定時器timer
和過時時間deadline
實現定時取消的功能。timerCtx
在調用cancel
方法,會先將內部的cancelCtx
取消,若是須要則將本身從cancelCtx
祖先節點上移除,最後取消計時器。
WithDeadline
返回一個基於parent
的可取消的context
,而且其過時時間deadline
不晚於所設置時間d
。
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,
}
// 創建新建context與可取消context祖先節點的取消關聯關係
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) }
}
複製代碼
parent
有過時時間而且過時時間早於給定時間d
,那麼新建的子節點context
無需設置過時時間,使用WithCancel
建立一個可取消的context
便可;parent
和過時時間d
建立一個定時取消的timerCtx
,並創建新建context
與可取消context
祖先節點的取消關聯關係,接下來判斷當前時間距離過時時間d
的時長dur
:dur
小於0,即當前已通過了過時時間,則直接取消新建的timerCtx
,緣由爲DeadlineExceeded
;timerCtx
設置定時器,一旦到達過時時間即取消當前timerCtx
。與WithDeadline
相似,WithTimeout
也是建立一個定時取消的context
,只不過WithDeadline
是接收一個過時時間點,而WithTimeout
接收一個相對當前時間的過時時長timeout
:
func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc) {
return WithDeadline(parent, time.Now().Add(timeout))
}
複製代碼
首先使用context
實現文章開頭done channel
的例子來示範一下如何更優雅實現協程間取消信號的同步:
func main() {
messages := make(chan int, 10)
// producer
for i := 0; i < 10; i++ {
messages <- i
}
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
// consumer
go func(ctx context.Context) {
ticker := time.NewTicker(1 * time.Second)
for _ = range ticker.C {
select {
case <-ctx.Done():
fmt.Println("child process interrupt...")
return
default:
fmt.Printf("send message: %d\n", <-messages)
}
}
}(ctx)
defer close(messages)
defer cancel()
select {
case <-ctx.Done():
time.Sleep(1 * time.Second)
fmt.Println("main process exit!")
}
}
複製代碼
這個例子中,只要讓子線程監聽主線程傳入的ctx
,一旦ctx.Done()
返回空channel
,子線程便可取消執行任務。但這個例子還沒法展示context
的傳遞取消信息的強大優點。
閱讀過net/http
包源碼的朋友可能注意到在實現http server
時就用到了context
, 下面簡單分析一下。
一、首先Server
在開啓服務時會建立一個valueCtx
,存儲了server
的相關信息,以後每創建一條鏈接就會開啓一個協程,並攜帶此valueCtx
。
func (srv *Server) Serve(l net.Listener) error {
...
var tempDelay time.Duration // how long to sleep on accept failure
baseCtx := context.Background() // base is always background, per Issue 16220
ctx := context.WithValue(baseCtx, ServerContextKey, srv)
for {
rw, e := l.Accept()
...
tempDelay = 0
c := srv.newConn(rw)
c.setState(c.rwc, StateNew) // before Serve can return
go c.serve(ctx)
}
}
複製代碼
二、創建鏈接以後會基於傳入的context
建立一個valueCtx
用於存儲本地地址信息,以後在此基礎上又建立了一個cancelCtx
,而後開始從當前鏈接中讀取網絡請求,每當讀取到一個請求則會將該cancelCtx
傳入,用以傳遞取消信號。一旦鏈接斷開,便可發送取消信號,取消全部進行中的網絡請求。
func (c *conn) serve(ctx context.Context) {
c.remoteAddr = c.rwc.RemoteAddr().String()
ctx = context.WithValue(ctx, LocalAddrContextKey, c.rwc.LocalAddr())
...
ctx, cancelCtx := context.WithCancel(ctx)
c.cancelCtx = cancelCtx
defer cancelCtx()
...
for {
w, err := c.readRequest(ctx)
...
serverHandler{c.server}.ServeHTTP(w, w.req)
...
}
}
複製代碼
三、讀取到請求以後,會再次基於傳入的context
建立新的cancelCtx
,並設置到當前請求對象req
上,同時生成的response
對象中cancelCtx
保存了當前context
取消方法。
func (c *conn) readRequest(ctx context.Context) (w *response, err error) {
...
req, err := readRequest(c.bufr, keepHostHeader)
...
ctx, cancelCtx := context.WithCancel(ctx)
req.ctx = ctx
...
w = &response{
conn: c,
cancelCtx: cancelCtx,
req: req,
reqBody: req.Body,
handlerHeader: make(Header),
contentLength: -1,
closeNotifyCh: make(chan bool, 1),
// We populate these ahead of time so we're not
// reading from req.Header after their Handler starts
// and maybe mutates it (Issue 14940)
wants10KeepAlive: req.wantsHttp10KeepAlive(),
wantsClose: req.wantsClose(),
}
...
return w, nil
}
複製代碼
這樣處理的目的主要有如下幾點:
一旦請求超時,便可中斷當前請求;
在處理構建response
過程當中若是發生錯誤,可直接調用response
對象的cancelCtx
方法結束當前請求;
在處理構建response
完成以後,調用response
對象的cancelCtx
方法結束當前請求。
在整個server
處理流程中,使用了一條context
鏈貫穿Server
、Connection
、Request
,不只將上游的信息共享給下游任務,同時實現了上游可發送取消信號取消全部下游任務,而下游任務自行取消不會影響上游任務。
context
主要用於父子任務之間的同步取消信號,本質上是一種協程調度的方式。另外在使用context
時有兩點值得注意:上游任務僅僅使用context
通知下游任務再也不須要,但不會直接干涉和中斷下游任務的執行,由下游任務自行決定後續的處理操做,也就是說context
的取消操做是無侵入的;context
是線程安全的,由於context
自己是不可變的(immutable
),所以能夠放心地在多個協程中傳遞使用。