GoLang并發(fā)控制(下)

context的字面意思是上下文站楚,是一個(gè)比較抽象的詞,字面上理解就是上下層的傳遞搏嗡,上會(huì)把內(nèi)容傳遞給下窿春,在go中程序單位一般為goroutine,這里的上下文便是在goroutine之間進(jìn)行傳遞采盒。

根據(jù)現(xiàn)實(shí)例子來講旧乞,最常看到context的便是web端磅氨。一個(gè)網(wǎng)絡(luò)請(qǐng)求request請(qǐng)求服務(wù)端尺栖,每一個(gè)request都會(huì)開啟一個(gè)goroutine,這個(gè)goroutine在邏輯處理中可能會(huì)去開啟其他的goroutine烦租,例如去開啟一個(gè)MongoDB的連接延赌,一個(gè)request的goroutine開啟了很多個(gè)goroutine時(shí)候除盏,需要對(duì)這些goroutine進(jìn)行控制,這時(shí)候就需要context來進(jìn)行對(duì)這些goroutine進(jìn)行跟蹤挫以。即一個(gè)請(qǐng)求Request者蠕,會(huì)需要多個(gè)Goroutine中處理。而這些Goroutine可能需要共享Request的一些信息掐松;同時(shí)當(dāng)Request被取消或者超時(shí)的時(shí)候蠢棱,所有從這個(gè)Request創(chuàng)建的所有Goroutine也應(yīng)該被結(jié)束。

例子講述完畢甩栈,用go的風(fēng)格再講一次泻仙。

在每一個(gè)goroutine在執(zhí)行之前,都要知道程序當(dāng)前的執(zhí)行狀態(tài)量没,這些狀態(tài)都被封裝在context變量中玉转,要傳遞給要執(zhí)行的goroutine中去,這個(gè)上下文就成為了傳遞與請(qǐng)求同生存周期變量的標(biāo)準(zhǔn)方法殴蹄。

注意 context是在go 1.7版本之后引入的究抓,以前版本的注意(go更新特別快,每一個(gè)版本都變得越來越好袭灯,自己第一次接觸go語言的時(shí)候才1.9版本刺下,實(shí)習(xí)公司用的好像是1.7,研發(fā)團(tuán)隊(duì)解體后現(xiàn)在實(shí)習(xí)用的版本是1.11 短時(shí)間版本就如此之大稽荧,1.10版本G-M模型改為G-P-M模型橘茉,聽聞1.12社區(qū)會(huì)再次優(yōu)化GC垃圾回收,引入分代)


Context接口
Context的接口定義的比較簡潔姨丈,我們看下這個(gè)接口的方法畅卓。

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

    Done() <-chan struct{}

    Err() error

    Value(key interface{}) interface{}
}

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

Deadline方法是獲取設(shè)置的截止時(shí)間的意思,第一個(gè)返回式是截止時(shí)間歼争,到了這個(gè)時(shí)間點(diǎn)拜马,Context會(huì)自動(dòng)發(fā)起取消請(qǐng)求;第二個(gè)返回值ok==false時(shí)表示沒有設(shè)置截止時(shí)間沐绒,如果需要取消的話俩莽,需要調(diào)用取消函數(shù)進(jìn)行取消。

Done方法返回一個(gè)只讀的chan洒沦,類型為struct{}豹绪,我們在goroutine中,如果該方法返回的chan可以讀取,則意味著parent context已經(jīng)發(fā)起了取消請(qǐng)求瞒津,我們通過Done方法收到這個(gè)信號(hào)后蝉衣,就應(yīng)該做清理操作,然后退出goroutine巷蚪,釋放資源病毡。

Err方法返回取消的錯(cuò)誤原因,因?yàn)槭裁碈ontext被取消屁柏。

Value方法獲取該Context上綁定的值啦膜,是一個(gè)鍵值對(duì),所以要通過一個(gè)Key才可以獲取對(duì)應(yīng)的值淌喻,這個(gè)值一般是線程安全的僧家。

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

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

這四個(gè)With函數(shù),接收的都有一個(gè)partent參數(shù)涯塔,就是父Context肌稻,我們要基于這個(gè)父Context創(chuàng)建出子Context的意思,這種方式可以理解為子Context對(duì)父Context的繼承匕荸,也可以理解為基于父Context的衍生爹谭。

通過這些函數(shù),就創(chuàng)建了一顆Context樹榛搔,樹的每個(gè)節(jié)點(diǎn)都可以有任意多個(gè)子節(jié)點(diǎn)诺凡,節(jié)點(diǎn)層級(jí)可以有任意多個(gè)。

WithCancel函數(shù)药薯,傳遞一個(gè)父Context作為參數(shù)绑洛,返回子Context,以及一個(gè)取消函數(shù)用來取消Context童本。 WithDeadline函數(shù),和WithCancel差不多脸候,它會(huì)多傳遞一個(gè)截止時(shí)間參數(shù)穷娱,意味著到了這個(gè)時(shí)間點(diǎn),會(huì)自動(dòng)取消Context运沦,當(dāng)然我們也可以不等到這個(gè)時(shí)候泵额,可以提前通過取消函數(shù)進(jìn)行取消。

WithTimeoutWithDeadline基本上一樣携添,這個(gè)表示是超時(shí)自動(dòng)取消嫁盲,是多少時(shí)間后自動(dòng)取消Context的意思。

WithValue函數(shù)和取消Context無關(guān)烈掠,它是為了生成一個(gè)綁定了一個(gè)鍵值對(duì)數(shù)據(jù)的Context羞秤,這個(gè)綁定的數(shù)據(jù)可以通過Context.Value方法訪問到


引用飛雪無情的代碼:

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

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

}

context.Background() 返回一個(gè)空的Context,這個(gè)空的Context一般用于整個(gè)Context樹的根節(jié)點(diǎn)哺哼。然后我們使用context.WithCancel(parent)函數(shù)佩抹,創(chuàng)建一個(gè)可取消的子Context,然后當(dāng)作參數(shù)傳給goroutine使用取董,這樣就可以使用這個(gè)子Context跟蹤這個(gè)goroutine棍苹。

在goroutine中,使用select調(diào)用<-ctx.Done()判斷是否要結(jié)束茵汰,如果接受到值的話廊勃,就可以返回結(jié)束goroutine了;如果接收不到经窖,就會(huì)繼續(xù)進(jìn)行監(jiān)控坡垫。

那么是如何發(fā)送結(jié)束指令的呢?這就是示例中的cancel函數(shù)啦画侣,它是我們調(diào)用context.WithCancel(parent)函數(shù)生成子Context的時(shí)候返回的冰悠,第二個(gè)返回值就是這個(gè)取消函數(shù),它是CancelFunc類型的配乱。我們調(diào)用它就可以發(fā)出取消指令溉卓,然后我們的監(jiān)控goroutine就會(huì)收到信號(hào),就會(huì)返回結(jié)束搬泥。

在引用一段多控制

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

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

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

示例中啟動(dòng)了3個(gè)監(jiān)控goroutine進(jìn)行不斷的監(jiān)控,每一個(gè)都使用了Context進(jìn)行跟蹤燥透,當(dāng)我們使用cancel函數(shù)通知取消時(shí)沙咏,這3個(gè)goroutine都會(huì)被結(jié)束。這就是Context的控制能力班套,它就像一個(gè)控制器一樣肢藐,按下開關(guān)后,所有基于這個(gè)Context或者衍生的子Context都會(huì)收到通知吱韭,這時(shí)就可以進(jìn)行清理操作了吆豹,最終釋放goroutine,這就優(yōu)雅的解決了goroutine啟動(dòng)后不可控的問題。


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

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()
            //啟動(dòng)子goroutine是為了不阻塞當(dāng)前goroutine痘煤,這里在實(shí)際場景中可以去執(zhí)行其他邏輯凑阶,這里為了方便直接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里一般會(huì)處理一些IO任務(wù),如讀寫數(shù)據(jù)庫或者rpc調(diào)用速勇,這里為了方便直接把數(shù)據(jù)打印
            fmt.Println("printing ", ctx.Value(favContextKey("resp")))
            time.Sleep(time.Second * 1)
        }
    }
}

首先調(diào)用context.Background()生成根節(jié)點(diǎn)晌砾,然后調(diào)用withCancel方法,傳入根節(jié)點(diǎn)烦磁,得到新的子Context以及根節(jié)點(diǎn)的cancel方法(通知所有子節(jié)點(diǎn)結(jié)束運(yùn)行)养匈,這里要注意:該方法也返回了一個(gè)Context,這是一個(gè)新的子節(jié)點(diǎn)都伪,與初始傳入的根節(jié)點(diǎn)不是同一個(gè)實(shí)例了呕乎,但是每一個(gè)子節(jié)點(diǎn)里會(huì)保存從最初的根節(jié)點(diǎn)到本節(jié)點(diǎn)的鏈路信息 ,才能實(shí)現(xiàn)鏈?zhǔn)健?/p>

程序的reqURL方法接收一個(gè)url陨晶,然后通過http請(qǐng)求該url獲得response猬仁,然后在當(dāng)前goroutine里再啟動(dòng)一個(gè)子groutine把response打印出來,然后從ReqURL開始Context樹往下衍生葉子節(jié)點(diǎn)(每一個(gè)鏈?zhǔn)秸{(diào)用新產(chǎn)生的ctx),中間每個(gè)ctx都可以通過WithValue方式傳值(實(shí)現(xiàn)通信)先誉,而每一個(gè)子goroutine都能通過Value方法從父goroutine取值湿刽,實(shí)現(xiàn)協(xié)程間的通信,每個(gè)子ctx可以調(diào)用Done方法檢測是否有父節(jié)點(diǎn)調(diào)用cancel方法通知子節(jié)點(diǎn)退出運(yùn)行褐耳,根節(jié)點(diǎn)的cancel調(diào)用會(huì)沿著鏈路通知到每一個(gè)子節(jié)點(diǎn)诈闺,因此實(shí)現(xiàn)了強(qiáng)并發(fā)控制,流程如圖:

044svco84sif9rjebqagmar0fp.png

context使用規(guī)范

最后铃芦,Context雖然是神器雅镊,但開發(fā)者使用也要遵循基本法,以下是一些Context使用的規(guī)范:

  • 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存在一個(gè)結(jié)構(gòu)體當(dāng)中仁烹,顯式地傳入函數(shù)。Context變量需要作為第一個(gè)參數(shù)使用咧虎,一般命名為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;即使方法允許老客,也不要傳入一個(gè)nil的Context僚饭,如果你不確定你要用什么Context的時(shí)候傳一個(gè)context.TODO;
  • Use context Values only for request-scoped data that transits processes and APIs, not for passing optional parameters to functions胧砰;使用context的Value相關(guān)方法只應(yīng)該用于在程序和接口中傳遞的和請(qǐng)求相關(guān)的元數(shù)據(jù),不要用它來傳遞一些可選的參數(shù)苇瓣;
  • The same Context may be passed to functions running in different goroutines; Contexts are safe for simultaneous use by multiple goroutines尉间;同樣的Context可以用來傳遞到不同的goroutine中,Context在多個(gè)goroutine中是安全的;
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末哲嘲,一起剝皮案震驚了整個(gè)濱河市贪薪,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌眠副,老刑警劉巖画切,帶你破解...
    沈念sama閱讀 206,311評(píng)論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異囱怕,居然都是意外死亡霍弹,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,339評(píng)論 2 382
  • 文/潘曉璐 我一進(jìn)店門娃弓,熙熙樓的掌柜王于貴愁眉苦臉地迎上來典格,“玉大人,你說我怎么就攤上這事台丛∷=桑” “怎么了?”我有些...
    開封第一講書人閱讀 152,671評(píng)論 0 342
  • 文/不壞的土叔 我叫張陵挽霉,是天一觀的道長防嗡。 經(jīng)常有香客問我,道長侠坎,這世上最難降的妖魔是什么蚁趁? 我笑而不...
    開封第一講書人閱讀 55,252評(píng)論 1 279
  • 正文 為了忘掉前任,我火速辦了婚禮硅蹦,結(jié)果婚禮上荣德,老公的妹妹穿的比我還像新娘。我一直安慰自己童芹,他們只是感情好涮瞻,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,253評(píng)論 5 371
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著假褪,像睡著了一般署咽。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上生音,一...
    開封第一講書人閱讀 49,031評(píng)論 1 285
  • 那天宁否,我揣著相機(jī)與錄音,去河邊找鬼缀遍。 笑死慕匠,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的域醇。 我是一名探鬼主播台谊,決...
    沈念sama閱讀 38,340評(píng)論 3 399
  • 文/蒼蘭香墨 我猛地睜開眼蓉媳,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了锅铅?” 一聲冷哼從身側(cè)響起酪呻,我...
    開封第一講書人閱讀 36,973評(píng)論 0 259
  • 序言:老撾萬榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎盐须,沒想到半個(gè)月后玩荠,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 43,466評(píng)論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡贼邓,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 35,937評(píng)論 2 323
  • 正文 我和宋清朗相戀三年阶冈,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片立帖。...
    茶點(diǎn)故事閱讀 38,039評(píng)論 1 333
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡眼溶,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出晓勇,到底是詐尸還是另有隱情堂飞,我是刑警寧澤,帶...
    沈念sama閱讀 33,701評(píng)論 4 323
  • 正文 年R本政府宣布绑咱,位于F島的核電站绰筛,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏描融。R本人自食惡果不足惜铝噩,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,254評(píng)論 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望窿克。 院中可真熱鬧骏庸,春花似錦、人聲如沸年叮。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,259評(píng)論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽只损。三九已至一姿,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間跃惫,已是汗流浹背叮叹。 一陣腳步聲響...
    開封第一講書人閱讀 31,485評(píng)論 1 262
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留爆存,地道東北人蛉顽。 一個(gè)月前我還...
    沈念sama閱讀 45,497評(píng)論 2 354
  • 正文 我出身青樓,卻偏偏與公主長得像先较,于是被迫代替她去往敵國和親蜂林。 傳聞我的和親對(duì)象是個(gè)殘疾皇子遥诉,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,786評(píng)論 2 345