Golang Context 是好的設計嗎?

最近實現系統的分佈式日誌與事務管理時,在尋求所謂的全局惟一Goroutine ID無果以後,決定仍是簡單利用Context機制實現了基本的想法,不夠高明,可是好用。因而對它當初的設計比較好奇,便有了此文。

一、What Context

Context是Golang官方定義的一個package,它定義了Context類型,裏面包含了Deadline/Done/Err方法以及綁定到Context上的成員變量值Value,具體定義以下:git

type Context interface {
    // 返回Context的超時時間(超時返回場景)
    Deadline() (deadline time.Time, ok bool)
    // 在Context超時或取消時(即結束了)返回一個關閉的channel
    // 即若是當前Context超時或取消時,Done方法會返回一個channel,而後其餘地方就能夠經過判斷Done方法是否有返回(channel),若是有則說明Context已結束
    // 故其能夠做爲廣播通知其餘相關方本Context已結束,請作相關處理。
    Done() <-chan struct{}

    // 返回Context取消的緣由
    Err() error
    
    // 返回Context相關數據
    Value(key interface{}) interface{}
}

那麼到底什麼Context?github

能夠字面意思能夠理解爲上下文,比較熟悉的有進程/線程上線文,關於Golang中的上下文,一句話歸納就是:goroutine的相關環境快照,其中包含函數調用以及涉及的相關的變量值。
經過Context能夠區分不一樣的goroutine請求,由於在Golang Severs中,每一個請求都是在單個goroutine中完成的。golang

:關於goroutine的理解能夠移步這裏數據庫

二、Why Context

因爲在Golang severs中,每一個request都是在單個goroutine中完成,而且在單個goroutine(不妨稱之爲A)中也會有請求其餘服務(啓動另外一個goroutine(稱之爲B)去完成)的場景,這就會涉及多個Goroutine之間的調用。若是某一時刻請求其餘服務被取消或者超時,則做爲深陷其中的當前goroutine B須要當即退出,而後系統纔可回收B所佔用的資源。
即一個request中一般包含多個goroutine,這些goroutine之間一般會有交互。
圖片描述分佈式

那麼,如何有效管理這些goroutine成爲一個問題(主要是退出通知和元數據傳遞問題),Google的解決方法是Context機制,相互調用的goroutine之間經過傳遞context變量保持關聯,這樣在不用暴露各goroutine內部實現細節的前提下,有效地控制各goroutine的運行。
圖片描述函數

如此一來,經過傳遞Context就能夠追蹤goroutine調用樹,並在這些調用樹之間傳遞通知和元數據。
雖然goroutine之間是平行的,沒有繼承關係,可是Context設計成是包含父子關係的,這樣能夠更好的描述goroutine調用之間的樹型關係。post

三、How to use

生成一個Context主要有兩類方法:google

3.1 )頂層Context:Background

要建立Context樹,首先就是要建立根節點spa

// 返回一個空的Context,它做爲全部由此繼承Context的根節點
func Background() Context

該Context一般由接收request的第一個goroutine建立,它不能被取消、沒有值、也沒有過時時間,常做爲處理request的頂層context存在。.net

3.2)下層Context:WithCancel/WithDeadline/WithTimeout

有了根節點以後,接下來就是建立子孫節點。爲了能夠很好的控制子孫節點,Context包提供的建立方法均是帶有第二返回值(CancelFunc類型),它至關於一個Hook,在子goroutine執行過程當中,能夠經過觸發Hook來達到控制子goroutine的目的(一般是取消,即讓其停下來)。再配合Context提供的Done方法,子goroutine能夠檢查自身是否被父級節點Cancel:

select { 
    case <-ctx.Done(): 
        // do some clean… 
}

:父節點Context能夠主動經過調用cancel方法取消子節點Context,而子節點Context只能被動等待。同時父節點Context自身一旦被取消(如其上級節點Cancel),其下的全部子節點Context均會自動被取消。

有三種建立方法:

// 帶cancel返回值的Context,一旦cancel被調用,即取消該建立的context
func WithCancel(parent Context) (ctx Context, cancel CancelFunc) 

// 帶有效期cancel返回值的Context,即必須到達指定時間點調用的cancel方法纔會被執行
func WithDeadline(parent Context, deadline time.Time) (Context, CancelFunc) 

// 帶超時時間cancel返回值的Context,相似Deadline,前者是時間點,後者爲時間間隔
// 至關於WithDeadline(parent, time.Now().Add(timeout)).
func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc)

下面來看改編自Advanced Go Concurrency Patterns視頻提供的一個簡單例子:

package main

import (
    "context"
    "fmt"
    "time"
)

func someHandler() {
    // 建立繼承Background的子節點Context
    ctx, cancel := context.WithCancel(context.Background())
    go doSth(ctx)

    //模擬程序運行 - Sleep 5秒
    time.Sleep(5 * time.Second)
    cancel()
}

//每1秒work一下,同時會判斷ctx是否被取消,若是是就退出
func doSth(ctx context.Context) {
    var i = 1
    for {
        time.Sleep(1 * time.Second)
        select {
        case <-ctx.Done():
            fmt.Println("done")
            return
        default:
            fmt.Printf("work %d seconds: \n", i)
        }
        i++
    }
}

func main() {
    fmt.Println("start...")
    someHandler()
    fmt.Println("end.")
}

輸出結果:

clipboard.png

注意,此時doSth方法中case之done的fmt.Println("done")並無被打印出來。

超時場景:

package main

import (
    "context"
    "fmt"
    "time"
)

func timeoutHandler() {
    // 建立繼承Background的子節點Context
    ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
    go doSth(ctx)

    //模擬程序運行 - Sleep 10秒
    time.Sleep(10 * time.Second)
    cancel() // 3秒後將提早取消 doSth goroutine
}

//每1秒work一下,同時會判斷ctx是否被取消,若是是就退出
func doSth(ctx context.Context) {
    var i = 1
    for {
        time.Sleep(1 * time.Second)
        select {
        case <-ctx.Done():
            fmt.Println("done")
            return
        default:
            fmt.Printf("work %d seconds: \n", i)
        }
        i++
    }
}

func main() {
    fmt.Println("start...")
    timeoutHandler()
    fmt.Println("end.")
}

輸出結果:

clipboard.png

四、Really elegant solution?

前面鋪地了這麼多。

確實,經過引入Context包,一個request範圍內全部goroutine運行時的取消能夠獲得有效的控制。可是這種解決方式卻不夠優雅。

4.1 Like a virus

一旦代碼中某處用到了Context,傳遞Context變量(一般做爲函數的第一個參數)會像病毒同樣蔓延在各處調用它的地方。好比在一個request中實現數據庫事務或者分佈式日誌記錄,建立的context,會做爲參數傳遞到任何有數據庫操做或日誌記錄需求的函數代碼處。即每個相關函數都必須增長一個context.Context類型的參數,且做爲第一個參數,這對無關代碼徹底是侵入式的。

更多詳細內容可參見:Michal Strba 的context-should-go-away-go2文章

Google Group上的討論可移步這裏

4.2 Context isn’t for cancellation

Context機制最核心的功能是在goroutine之間傳遞cancel信號,可是它的實現是不徹底的。

Cancel能夠細分爲主動與被動兩種,經過傳遞context參數,讓調用goroutine能夠主動cancel被調用goroutine。可是如何得知被調用goroutine何時執行完畢,這部分Context機制是沒有實現的。而現實中的確又有一些這樣的場景,好比一個組裝數據的goroutine必須等待其餘goroutine完成纔可開始執行,這是context明顯不夠用了,必須藉助sync.WaitGroup。

func serve(l net.Listener) error {
        var wg sync.WaitGroup
        var conn net.Conn
        var err error
        for {
                conn, err = l.Accept()
                if err != nil {
                        break
                }
                wg.Add(1)
                go func(c net.Conn) {
                        defer wg.Done()
                        handle(c)
                }(conn)
        }
        wg.Wait()
        return err
}

五、Summary

https://golang.org/pkg/context/
https://faiface.github.io/pos...
https://juejin.im/entry/58088...
https://dave.cheney.net/2017/...
https://dave.cheney.net/2017/...
https://sites.google.com/site...

相關文章
相關標籤/搜索