Golang並行存取模型之Context詳解

來源:互聯網
上載者:User

 

對於 Golang 開發人員來說context(上下文)包一定不會陌生。但很多時候,我們懶惰的只是見過它,或能起到什麼作用,並不會去深究它。

應用情境:在 Go http 包的 Server 中,每一個請求在都有一個對應的goroutine去處理。請求處理函數通常會啟動額外的goroutine用來訪問後端服務,比如資料庫和 RPC 服務。用來處理一個請求的goroutine通常需要訪問一些與請求特定的資料,比如終端使用者的身份認證資訊、驗證相關的 token、請求的截止時間。當一個請求被取消或逾時時,所有用來處理該請求的goroutine都應該迅速退出,然後系統才能釋放這些goroutine佔用的資源,官方部落格。

注意:go1.6及之前版本請使用golang.org/x/net/contextgo1.7及之後已移到標準庫context

Context 原理

Context 的調用應該是鏈式的,通過WithCancelWithDeadlineWithTimeoutWithValue派生出新的 Context。當父 Context 被取消時,其派生的所有 Context 都將取消。

通過context.WithXXX都將返回新的 Context 和 CancelFunc。調用 CancelFunc 將取消子代,移除父代對子代的引用,並且停止所有定時器。未能調用 CancelFunc 將泄漏子代,直到父代被取消或定時器觸發。go vet工具檢查所有流程式控制制路徑上使用 CancelFuncs。

遵循規則

遵循以下規則,以保持包之間的介面一致,並啟用靜態分析工具以檢查上下文傳播。

  1. 不要將 Contexts 放入結構體,相反context應該作為第一個參數傳入,命名為ctx。 func DoSomething(ctx context.Context,arg Arg)error { // ... use ctx ... }
  2. 即使函數允許,也不要傳入nil的 Context。如果不知道用哪種 Context,可以使用context.TODO()
  3. 使用context的Value相關方法只應該用於在程式和介面中傳遞的和請求相關的中繼資料,不要用它來傳遞一些可選的參數
  4. 相同的 Context 可以傳遞給在不同的goroutine;Context 是並發安全的。

Context 包

Context 結構體。

// A Context carries a deadline, cancelation signal, and request-scoped values// across API boundaries. Its methods are safe for simultaneous use by multiple// goroutines.type Context interface {    // Done returns a channel that is closed when this Context is canceled    // or times out.    Done() <-chan struct{}    // Err indicates why this context was canceled, after the Done channel    // is closed.    Err() error    // Deadline returns the time when this Context will be canceled, if any.    Deadline() (deadline time.Time, ok bool)    // Value returns the value associated with key or nil if none.    Value(key interface{}) interface{}}
  • Done(),返回一個channel。當times out或者調用cancel方法時,將會close掉。
  • Err(),返回一個錯誤。該context為什麼被取消掉。
  • Deadline(),返回截止時間和ok。
  • Value(),傳回值。

所有方法

func Background() Contextfunc TODO() Contextfunc WithCancel(parent Context) (ctx Context, cancel CancelFunc)func WithDeadline(parent Context, deadline time.Time) (Context, CancelFunc)func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc)func WithValue(parent Context, key, val interface{}) Context

上面可以看到Context是一個介面,想要使用就得實現其方法。在context包內部已經為我們實現好了兩個空的Context,可以通過調用Background()和TODO()方法擷取。一般的將它們作為Context的根,往下派生。

WithCancel 例子

WithCancel 以一個新的 Done channel 返回一個父 Context 的拷貝。

   229  func WithCancel(parent Context) (ctx Context, cancel CancelFunc) {   230      c := newCancelCtx(parent)   231      propagateCancel(parent, &c)   232      return &c, func() { c.cancel(true, Canceled) }   233  }   234     235  // newCancelCtx returns an initialized cancelCtx.   236  func newCancelCtx(parent Context) cancelCtx {   237      return cancelCtx{   238          Context: parent,   239          done:    make(chan struct{}),   240      }   241  }

此樣本示範使用一個可取消的上下文,以防止 goroutine 泄漏。樣本函數結束時,defer 調用 cancel 方法,gen goroutine 將返回而不泄漏。

package mainimport (    "context"    "fmt")func main() {    // gen generates integers in a separate goroutine and    // sends them to the returned channel.    // The callers of gen need to cancel the context once    // they are done consuming generated integers not to leak    // the internal goroutine started by gen.    gen := func(ctx context.Context) <-chan int {        dst := make(chan int)        n := 1        go func() {            for {                select {                case <-ctx.Done():                    return // returning not to leak the goroutine                case dst <- n:                    n++                }            }        }()        return dst    }    ctx, cancel := context.WithCancel(context.Background())    defer cancel() // cancel when we are finished consuming integers    for n := range gen(ctx) {        fmt.Println(n)        if n == 5 {            break        }    }}

WithDeadline 例子

   369  func WithDeadline(parent Context, deadline time.Time) (Context, CancelFunc) {   370      if cur, ok := parent.Deadline(); ok && cur.Before(deadline) {   371          // The current deadline is already sooner than the new one.   372          return WithCancel(parent)   373      }   374      c := &timerCtx{   375          cancelCtx: newCancelCtx(parent),   376          deadline:  deadline,   377      }   ......

可以清晰的看到,當派生出的子 Context 的deadline在父Context之後,直接返回了一個父Context的拷貝。故語義上等效為父。

WithDeadline 的期限調整為不晚於 d 返回父內容相關的副本。如果父母的到期日已經早於 d,WithDeadline (父,d) 是在語義上等效為父。返回的上下文完成的通道關閉的期限期滿後,返回的取消函數調用時,或當父上下文完成的通道關閉,以先發生者為準。

看看官方例子:

package mainimport (    "context"    "fmt"    "time")func main() {    d := time.Now().Add(50 * time.Millisecond)    ctx, cancel := context.WithDeadline(context.Background(), d)    // Even though ctx will be expired, it is good practice to call its    // cancelation function in any case. Failure to do so may keep the    // context and its parent alive longer than necessary.    defer cancel()    select {    case <-time.After(1 * time.Second):        fmt.Println("overslept")    case <-ctx.Done():        fmt.Println(ctx.Err())    }}

WithTimeout 例子

WithTimeout 返回 WithDeadline(parent, time.Now().Add(timeout))。

   436  func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc) {   437      return WithDeadline(parent, time.Now().Add(timeout))   438  }

看看官方例子:

package mainimport (    "context"    "fmt"    "time")func main() {    // Pass a context with a timeout to tell a blocking function that it    // should abandon its work after the timeout elapses.    ctx, cancel := context.WithTimeout(context.Background(), 50*time.Millisecond)    defer cancel()    select {    case <-time.After(1 * time.Second):        fmt.Println("overslept")    case <-ctx.Done():        fmt.Println(ctx.Err()) // prints "context deadline exceeded"    }}

WithValue 例子

   454  func WithValue(parent Context, key, val interface{}) Context {   454      if key == nil {   455          panic("nil key")   456      }   457      if !reflect.TypeOf(key).Comparable() {   458          panic("key is not comparable")   459      }   460      return &valueCtx{parent, key, val}   461  }

WithValue 返回的父與鍵關聯的值在 val 的副本。

使用上下文值僅為過渡進程和 Api 的請求範圍的資料,而不是將選擇性參數傳遞給函數。

提供的鍵必須是可比性和應該不是字串類型或任何其他內建的類型以避免包使用的上下文之間的碰撞。WithValue 使用者應該定義自己的鍵的類型。為了避免分配分配給介面 {} 時,上下文鍵經常有具體類型結構 {}。另外,匯出的上下文關鍵變數靜態類型應該是一個指標或介面。

看看官方例子:

package mainimport (    "context"    "fmt")func main() {    type favContextKey string    f := func(ctx context.Context, k favContextKey) {        if v := ctx.Value(k); v != nil {            fmt.Println("found value:", v)            return        }        fmt.Println("key not found:", k)    }    k := favContextKey("language")    ctx := context.WithValue(context.Background(), k, "Go")    f(ctx, k)    f(ctx, favContextKey("color"))}

參考串連

[1] https://segmentfault.com/a/1190000006744213
[2] http://www.01happy.com/golang-context-reading/

 

 

 

我的部落格即將入駐“雲棲社區”,誠邀技術同仁一同入駐。

相關文章

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.