天天看點

074-Context

上一篇文章結束我留下了一下小小的懸念,說 Golang 标準庫為我們提供了純天然的取消并發請求的解決方案,在這裡我就需要填坑了。

1. 取消并發請求

為了能讓你快速上手 context,我們繼續延續上一篇文章的取消并發請求的例子,使用 context 将其改寫。如果你不熟悉上一篇的例子,請務必回去再看一遍。

package main

import (
    "context"
    "fmt"
    "math/rand"
    "os"
    "sync"
    "time"
)

// Get 的參數由 channel 換成了 context
func Get(ctx context.Context) string {
    duration := rand.Intn(5) + 2
    tick := time.After(time.Duration(duration) * time.Second)
    select {
    case <-tick:
        return fmt.Sprintf("get page %d", duration)
    // 監控 context
    case <-ctx.Done():
        return fmt.Sprintf("cancel %d, reason:%v", duration, ctx.Err())
    }
}

func main() {
    rand.Seed(time.Now().Unix())
    // 建立一個 context 對象
    ctx, cancel := context.WithCancel(context.Background())
    var wg sync.WaitGroup
    go func() {
        os.Stdin.Read(make([]byte, 1))
        // 取消 context
        cancel()
    }()
    wg.Add(3)
    go func() {
        fmt.Println(Get(ctx))
        wg.Done()
    }()
    go func() {
        fmt.Println(Get(ctx))
        wg.Done()
    }()
    go func() {
        fmt.Println(Get(ctx))
        wg.Done()
    }()
    wg.Wait()
}      

這段程式同樣可以達到效果。實際上,上面的 Context 和之前我們使用 close channel 的功能是一樣的,甚至 Context 底層的實作,也使用 close channel,隻不過,Context 将這些操作都進行了封裝。

可能你會問,這不是将事情複雜化了嗎?看起來它并沒有使用 close channel 更加友善。看起來似乎是這樣,不過,Context 如果隻有這點功能,那的确不如直接使用 close channel。如果你仔細看 Context 的名字,它似乎和 cancel 請求并沒多大聯系,隻不過它提供了這樣的功能。

2. Context 介紹

在伺服器開發中,通常每個請求都會開啟一個 goroutine 去執行(多線程模型思想)。在每個請求裡,通常還會再開啟額外的 goroutine 用來通路下遊服務(典型的比如請求資料庫等)。這些額外的和請求相關的 goroutine 可能需要一些特定的參數或值,比如本次請求的辨別(ID),使用者的 session 資訊。

在 C/C++ 伺服器開發中,通常可以借助 TLS (線程局部存儲) 來存儲相關的資訊。而在 golang 裡,可以使用 Context 對象來完成類似的功能。

另一方面,當請求逾時或者被取消時,所有處理該請求的 goroutine 都應迅速退出,以便系統能回收它們正在使用的任何資源。

谷歌發明了 context 包,它可以很容易的将 request-scoped 相關的值、取消信号、過期時間通過 api 接口傳遞給所有的處理本次請求相關的 goroutines。

正如第 1 節中的示例一樣,我們使用了 context 來完成所有 goroutine 的取消。

Context 當然遠不止有取消請求的功能。

2.1 Context 接口

在 context 包中,Context 是一個接口類型,定義如下:

type Context interface {
    // 傳回 context 截止時間。如果沒有設定截止時間,則 ok == false
    Deadline() (deadline time.Time, ok bool)
    // 如果 context 被取消,Done 傳回的 channel 則被關閉。如果 context 無法被取消,則 Done 傳回 nil.
    Done() <-chan struct{}
    // context 被取消的原因
    Err() error
    // 擷取該 context 中儲存的 key-value
    Value(key interface{}) interface{}
}      

首先需要明确一點的是:

  • Context 是接口
  • 實作了 Context 接口的類型,不一定非得要完成所有的功能,比如截止時間。

下面是一個最簡單的實作了 Context 接口的類型 emptyCtx:

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"      

當然了,這個 Context 對象幾乎什麼也沒做。不過,這也為我們自己編寫屬于自己的 Context 提供了一些便利。比如:

type CancelContext struct {
    Context // 内嵌 Context 接口
    //...
}

// 繼承一個 emptyCtx,看起來有點像 Nodejs 的原型鍊繼承      

當然了,實際編碼中,我們基本上不必自己再去實作一個 Context 對象了。context 包裡已經為我們提供了一些已經實作好了的 Context 類型。主要是以下幾種:

  • CancelContext: 支援取消功能的 Context
  • DeadlineContext: 支援到期自動取消的 Context
  • TimeoutContext: 支援逾時自動取消的 Context
  • ValueContext: 能儲存 key-value 的 Context

你可以通過下面 4 個函數來生成不同功能的 Context 對象:

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

如果你自己看看上面這些函數的第一個參數,你會發現,它也是一個 Context 對象。這種利用父 context 來生成子 context 的思想,看起來簡直和 Nodejs 中的原型繼承一模一樣(沒了解過 Nodejs 的就忽略這句話啦)。

既然如此,總得有一個“上帝 Context”吧,這個上帝 Context 你完全可以自己折騰一個出來,當然你也可以使用上面我們寫好的 emptyCtx 的對象。不過,Context 包已經為我們考慮好了,造輪的事情你也不用操心,你可以使用下面的函數來拿到一個 emptyCtx 對象:

func      

一切 Context 都是由這個 emptyCtx 繁衍而來。這看起來像“道生一,一生二,二生三,三生萬物”的思想。“道”,指的就是那個 Background 函數。

2.2 Context 樹

基于上面的思想,通過不斷的基于 parent context 來繁衍新的 context 對象,最後形成的就是一棵 context 樹。

074-Context

圖1 河流分支

這意味着如果頂層的 Context 被 cancel,那麼所有的子 Context 也會被 cancel. 有人會問,為什麼不使用同一個 Context 對象呢,不也是一樣的功能嗎?正如第 1 節中的那樣,隻用一個 CancelContext 不就好了。

有一種情況,比如我隻想取消第 1 節中的某一個 Get 請求,你要怎麼辦呢?我隻想取消子 Context,而其它不受影響。這時候就能展現 Context 樹的必要性了。

3. 使用 Context 的示例

使用 CancelContext 的例子在第一節已經展示過了,這裡就不再展示。下面再舉一個使用 TimeoutContext 和 ValueContext 的例子。

3.1 TimeoutContext

package main

import (
    "context"
    "errors"
    "fmt"
    "math/rand"
    "os"
    "sync"
    "time"
)

func Get(ctx context.Context) string {
    duration := rand.Intn(5) + 2
    tick := time.After(time.Duration(duration) * time.Second)
    select {
    case <-tick:
        return fmt.Sprintf("get page %d", duration)
    case <-ctx.Done():
        return fmt.Sprintf("cancel %d, reason:%v", duration, ctx.Err())
    }
}

func main() {
    rand.Seed(time.Now().Unix())
    ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
    var wg sync.WaitGroup
    go func() {
        os.Stdin.Read(make([]byte, 1))
        context.Canceled = errors.New("手動取消請求")
        cancel()
    }()
    wg.Add(3)
    go func() {
        fmt.Println(Get(ctx))
        wg.Done()
    }()
    go func() {
        fmt.Println(Get(ctx))
        wg.Done()
    }()
    go func() {
        fmt.Println(Get(ctx))
        wg.Done()
    }()
    wg.Wait()
}      
074-Context

圖2 TimeoutContext

3.2 ValueContext

package main

import (
    "context"
    "errors"
    "fmt"
    "math/rand"
    "os"
    "sync"
    "time"
)

func Get(ctx context.Context) string {
    duration := rand.Intn(5) + 2
    tick := time.After(time.Duration(duration) * time.Second)
    select {
    case <-tick:
        return fmt.Sprintf("get page %d, session_id:%s, name:%s",
            duration, ctx.Value("session_id").(string), ctx.Value("name").(string))
    case <-ctx.Done():
        return fmt.Sprintf("cancel %d, reason:%v", duration, ctx.Err())
    }
}

func main() {
    rand.Seed(time.Now().Unix())
    // 先生成 CancelContext
    ctx, cancel := context.WithCancel(context.Background())
    // 生成 ValueContext
    ctx = context.WithValue(ctx, "session_id", "12345678")
    var wg sync.WaitGroup
    go func() {
        os.Stdin.Read(make([]byte, 1))
        context.Canceled = errors.New("手動取消請求")
        cancel()
    }()
    wg.Add(3)
    go func() {
        // 再為本次請求生成一個 ValueContext
        ctx := context.WithValue(ctx, "name", "allen")
        fmt.Println(Get(ctx))
        wg.Done()
    }()
    go func() {
        ctx := context.WithValue(ctx, "name", "luffy")
        fmt.Println(Get(ctx))
        wg.Done()
    }()
    go func() {
        ctx := context.WithValue(ctx, "name", "zoro")
        fmt.Println(Get(ctx))
        wg.Done()
    }()
    wg.Wait()
}      
074-Context

圖3 ValueContext

下面是 context 樹:

074-Context

4. 總結

  • 掌握 Context 的作用
  • 了解 Context 樹