GoLang并發控制(下)

context的字面意思是上下文,是一個比較抽象的詞,字面上理解就是上下層的傳遞,上會把內容傳遞給下,在go中程序單位一般為goroutine,這里的上下文便是在goroutine之間進行傳遞。

根據現實例子來講,最??吹絚ontext的便是web端。一個網絡請求request請求服務端,每一個request都會開啟一個goroutine,這個goroutine在邏輯處理中可能會去開啟其他的goroutine,例如去開啟一個MongoDB的連接,一個request的goroutine開啟了很多個goroutine時候,需要對這些goroutine進行控制,這時候就需要context來進行對這些goroutine進行跟蹤。即一個請求Request,會需要多個Goroutine中處理。而這些Goroutine可能需要共享Request的一些信息;同時當Request被取消或者超時的時候,所有從這個Request創建的所有Goroutine也應該被結束。

例子講述完畢,用go的風格再講一次。

在每一個goroutine在執行之前,都要知道程序當前的執行狀態,這些狀態都被封裝在context變量中,要傳遞給要執行的goroutine中去,這個上下文就成為了傳遞與請求同生存周期變量的標準方法。

注意 context是在go 1.7版本之后引入的,以前版本的注意(go更新特別快,每一個版本都變得越來越好,自己第一次接觸go語言的時候才1.9版本,實習公司用的好像是1.7,研發團隊解體后現在實習用的版本是1.11 短時間版本就如此之大,1.10版本G-M模型改為G-P-M模型,聽聞1.12社區會再次優化GC垃圾回收,引入分代)


Context接口
Context的接口定義的比較簡潔,我們看下這個接口的方法。

type Context interface {
    Deadline() (deadline time.Time, ok bool)

    Done() <-chan struct{}

    Err() error

    Value(key interface{}) interface{}
}

這個接口共有4個方法,了解這些方法的意思非常重要,這樣我們才可以更好的使用他們。

Deadline方法是獲取設置的截止時間的意思,第一個返回式是截止時間,到了這個時間點,Context會自動發起取消請求;第二個返回值ok==false時表示沒有設置截止時間,如果需要取消的話,需要調用取消函數進行取消。

Done方法返回一個只讀的chan,類型為struct{},我們在goroutine中,如果該方法返回的chan可以讀取,則意味著parent context已經發起了取消請求,我們通過Done方法收到這個信號后,就應該做清理操作,然后退出goroutine,釋放資源。

Err方法返回取消的錯誤原因,因為什么Context被取消。

Value方法獲取該Context上綁定的值,是一個鍵值對,所以要通過一個Key才可以獲取對應的值,這個值一般是線程安全的。

有了如上的根Context,那么是如何衍生更多的子Context的呢?這就要靠context包為我們提供的With系列的函數了。

Context的繼承衍生

func 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

這四個With函數,接收的都有一個partent參數,就是父Context,我們要基于這個父Context創建出子Context的意思,這種方式可以理解為子Context對父Context的繼承,也可以理解為基于父Context的衍生。

通過這些函數,就創建了一顆Context樹,樹的每個節點都可以有任意多個子節點,節點層級可以有任意多個。

WithCancel函數,傳遞一個父Context作為參數,返回子Context,以及一個取消函數用來取消Context。 WithDeadline函數,和WithCancel差不多,它會多傳遞一個截止時間參數,意味著到了這個時間點,會自動取消Context,當然我們也可以不等到這個時候,可以提前通過取消函數進行取消。

WithTimeoutWithDeadline基本上一樣,這個表示是超時自動取消,是多少時間后自動取消Context的意思。

WithValue函數和取消Context無關,它是為了生成一個綁定了一個鍵值對數據的Context,這個綁定的數據可以通過Context.Value方法訪問到


引用飛雪無情的代碼:

func main() {
    ctx, cancel := context.WithCancel(context.Background())
    go func(ctx context.Context) {
        for {
            select {
            case <-ctx.Done():
                fmt.Println("監控退出,停止了...")
                return
            default:
                fmt.Println("goroutine監控中...")
                time.Sleep(2 * time.Second)
            }
        }
    }(ctx)

    time.Sleep(10 * time.Second)
    fmt.Println("可以了,通知監控停止")
    cancel()
    //為了檢測監控過是否停止,如果沒有監控輸出,就表示停止了
    time.Sleep(5 * time.Second)

}

context.Background() 返回一個空的Context,這個空的Context一般用于整個Context樹的根節點。然后我們使用context.WithCancel(parent)函數,創建一個可取消的子Context,然后當作參數傳給goroutine使用,這樣就可以使用這個子Context跟蹤這個goroutine。

在goroutine中,使用select調用<-ctx.Done()判斷是否要結束,如果接受到值的話,就可以返回結束goroutine了;如果接收不到,就會繼續進行監控。

那么是如何發送結束指令的呢?這就是示例中的cancel函數啦,它是我們調用context.WithCancel(parent)函數生成子Context的時候返回的,第二個返回值就是這個取消函數,它是CancelFunc類型的。我們調用它就可以發出取消指令,然后我們的監控goroutine就會收到信號,就會返回結束。

在引用一段多控制

func main() {
    ctx, cancel := context.WithCancel(context.Background())
    go watch(ctx,"【監控1】")
    go watch(ctx,"【監控2】")
    go watch(ctx,"【監控3】")

    time.Sleep(10 * time.Second)
    fmt.Println("可以了,通知監控停止")
    cancel()
    //為了檢測監控過是否停止,如果沒有監控輸出,就表示停止了
    time.Sleep(5 * time.Second)
}

func watch(ctx context.Context, name string) {
    for {
        select {
        case <-ctx.Done():
            fmt.Println(name,"監控退出,停止了...")
            return
        default:
            fmt.Println(name,"goroutine監控中...")
            time.Sleep(2 * time.Second)
        }
    }
}

示例中啟動了3個監控goroutine進行不斷的監控,每一個都使用了Context進行跟蹤,當我們使用cancel函數通知取消時,這3個goroutine都會被結束。這就是Context的控制能力,它就像一個控制器一樣,按下開關后,所有基于這個Context或者衍生的子Context都會收到通知,這時就可以進行清理操作了,最終釋放goroutine,這就優雅的解決了goroutine啟動后不可控的問題。


在引用一次潘少大佬的代碼:

package main

import (
    "context"
    "crypto/md5"
    "fmt"
    "io/ioutil"
    "net/http"
    "sync"
    "time"
)

type favContextKey string

func main() {
    wg := &sync.WaitGroup{}
    values := []string{"https://www.baidu.com/", "https://www.zhihu.com/"}
    ctx, cancel := context.WithCancel(context.Background())

    for _, url := range values {
        wg.Add(1)
        subCtx := context.WithValue(ctx, favContextKey("url"), url)
        go reqURL(subCtx, wg)
    }

    go func() {
        time.Sleep(time.Second * 3)
        cancel()
    }()

    wg.Wait()
    fmt.Println("exit main goroutine")
}

func reqURL(ctx context.Context, wg *sync.WaitGroup) {
    defer wg.Done()
    url, _ := ctx.Value(favContextKey("url")).(string)
    for {
        select {
        case <-ctx.Done():
            fmt.Printf("stop getting url:%s\n", url)
            return
        default:
            r, err := http.Get(url)
            if r.StatusCode == http.StatusOK && err == nil {
                body, _ := ioutil.ReadAll(r.Body)
                subCtx := context.WithValue(ctx, favContextKey("resp"), fmt.Sprintf("%s%x", url, md5.Sum(body)))
                wg.Add(1)
                go showResp(subCtx, wg)
            }
            r.Body.Close()
            //啟動子goroutine是為了不阻塞當前goroutine,這里在實際場景中可以去執行其他邏輯,這里為了方便直接sleep一秒
            // doSometing()
            time.Sleep(time.Second * 1)
        }
    }
}

func showResp(ctx context.Context, wg *sync.WaitGroup) {
    defer wg.Done()
    for {
        select {
        case <-ctx.Done():
            fmt.Println("stop showing resp")
            return
        default:
            //子goroutine里一般會處理一些IO任務,如讀寫數據庫或者rpc調用,這里為了方便直接把數據打印
            fmt.Println("printing ", ctx.Value(favContextKey("resp")))
            time.Sleep(time.Second * 1)
        }
    }
}

首先調用context.Background()生成根節點,然后調用withCancel方法,傳入根節點,得到新的子Context以及根節點的cancel方法(通知所有子節點結束運行),這里要注意:該方法也返回了一個Context,這是一個新的子節點,與初始傳入的根節點不是同一個實例了,但是每一個子節點里會保存從最初的根節點到本節點的鏈路信息 ,才能實現鏈式。

程序的reqURL方法接收一個url,然后通過http請求該url獲得response,然后在當前goroutine里再啟動一個子groutine把response打印出來,然后從ReqURL開始Context樹往下衍生葉子節點(每一個鏈式調用新產生的ctx),中間每個ctx都可以通過WithValue方式傳值(實現通信),而每一個子goroutine都能通過Value方法從父goroutine取值,實現協程間的通信,每個子ctx可以調用Done方法檢測是否有父節點調用cancel方法通知子節點退出運行,根節點的cancel調用會沿著鏈路通知到每一個子節點,因此實現了強并發控制,流程如圖:

044svco84sif9rjebqagmar0fp.png

context使用規范

最后,Context雖然是神器,但開發者使用也要遵循基本法,以下是一些Context使用的規范:

  • Do not store Contexts inside a struct type; instead, pass a Context explicitly to each function that needs it. The Context should be the first parameter, typically named ctx;不要把Context存在一個結構體當中,顯式地傳入函數。Context變量需要作為第一個參數使用,一般命名為ctx;
  • Do not pass a nil Context, even if a function permits it. Pass context.TODO if you are unsure about which Context to use;即使方法允許,也不要傳入一個nil的Context,如果你不確定你要用什么Context的時候傳一個context.TODO;
  • Use context Values only for request-scoped data that transits processes and APIs, not for passing optional parameters to functions;使用context的Value相關方法只應該用于在程序和接口中傳遞的和請求相關的元數據,不要用它來傳遞一些可選的參數;
  • The same Context may be passed to functions running in different goroutines; Contexts are safe for simultaneous use by multiple goroutines;同樣的Context可以用來傳遞到不同的goroutine中,Context在多個goroutine中是安全的;
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,505評論 6 533
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,556評論 3 418
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,463評論 0 376
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,009評論 1 312
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,778評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,218評論 1 324
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,281評論 3 441
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,436評論 0 288
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,969評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,795評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,993評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,537評論 5 359
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,229評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,659評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,917評論 1 286
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,687評論 3 392
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,990評論 2 374