【Go上下文Context】

協(xié)程如何退出

一個(gè)協(xié)程啟動(dòng)后庆冕,大部分情況需要等待里面的代碼執(zhí)行完畢,然后協(xié)程會(huì)自動(dòng)退出遏乔。但是如果有一種情景,需要讓協(xié)程提前退出怎么辦囚枪?
func main() {
    var wg sync.WaitGroup
    wg.Add(1)
    go func(){
        defer wg.Done()
        watchDog("[監(jiān)控狗]")
    }()
    wg.Wait()
}


func watchDog(name string) {
    // 開(kāi)啟for select循環(huán),一直在后臺(tái)監(jiān)控
    for {
        select {
            default :
            fmt.Println(name, "正在監(jiān)控……")
        }
        
        time.Sleep(2 * time.Second)
    }
}

// 通過(guò)watchDog函數(shù)實(shí)現(xiàn)了一個(gè)監(jiān)控狗劳淆,他會(huì)在后臺(tái)一直運(yùn)行链沼,每個(gè)2秒答應(yīng)一串字符串
如果需要讓監(jiān)控狗停止監(jiān)控、退出程序沛鸵,一個(gè)辦法是定義全局變量括勺,其他地方可以通過(guò)修改這個(gè)全局變量發(fā)出停止監(jiān)控狗的通知,然后在協(xié)程中先檢查這個(gè)變量曲掰,如果發(fā)現(xiàn)被通知關(guān)閉疾捍,退出當(dāng)前協(xié)程。但是這個(gè)方法需要通過(guò)加鎖來(lái)保證多協(xié)程并發(fā)的安全栏妖,基于這個(gè)思路乱豆,升級(jí)版方案:用select + channel 做檢測(cè):
func main() {
    var wg sync.WaitGroup
    wg.Add(1)
    stopCh := make(chan bool)
    go func(){
        defer wg.Down()
        watchDog(stopCh, "監(jiān)控狗")
    }()
    time.Sleep(5 * time.Second)
    stopCh <- true
    wg.Wait()
}

func watchDog(stopCh chan bool, name string){
    for {
        select {
        case <- stopCh :
            fmt.Println(name, "停止監(jiān)控")
            return
        default :
            fmt.Println(name, "正在監(jiān)控")
        }
        
        time.Sleep(1 * time.Second)
    }
}

以上是使用select + channel方式改造watchDog函數(shù),實(shí)現(xiàn)了通過(guò)channel發(fā)送指令讓監(jiān)控狗停止吊趾,進(jìn)而達(dá)到協(xié)程退出的目的宛裕。

初識(shí)Context

通過(guò) **select+ channel** 讓協(xié)程退出的方式比較優(yōu)雅,但是如果我們需要做到同事取消很多協(xié)程呢趾徽?如果是定時(shí)取消呢续滋?這時(shí)候select+ channel的局限性就凸顯出來(lái)了,即使定義了多個(gè)channel解決問(wèn)題孵奶,代碼邏輯也會(huì)非常復(fù)雜疲酌、難以維護(hù)。要解決這種復(fù)雜的協(xié)程問(wèn)題了袁,必須要有一種**可以跟蹤協(xié)程的方案朗恳,只有跟蹤到每個(gè)協(xié)程,才能更好的控制他們载绿,這種方案就是Go語(yǔ)言標(biāo)準(zhǔn)庫(kù)為我們提供的Contex**粥诫。
func main() {
    var wg sync.WaitGroup
    wg.Add(1)
    ctx, stop := context.WithCancel(context.Background())
    go func(){
        defer wg.Down()
        watchDog(ctx, "監(jiān)控狗")
    }()
    time.Sleep(5 * time.Second)
    stop()
    wg.Wait()
}

func watchDog(stopCh chan bool, name string){
    for {
        select {
        case <- ctx.Done() :
            fmt.Println(name, "停止監(jiān)控")
            return
        default :
            fmt.Println(name, "正在監(jiān)控")
        }
        
        time.Sleep(1 * time.Second)
    }
}

相比select + channel 方案,Context方案主要有4個(gè)改動(dòng)點(diǎn):

  1. watchDog函數(shù)的stopCh 參數(shù)換成了ctx崭庸,類型為context.Context
  2. 原來(lái)case <- stopCh 改為 ctx.Done()怀浆,用于判斷是否停止
  3. 使用context.WithCancel(context.Background())函數(shù)生成一個(gè)可以取消的Context,用于發(fā)送停止指令怕享。這里的context.Background()用于生成一個(gè)空的Context执赡,一般作為整個(gè)Context樹(shù)的根節(jié)點(diǎn)
  4. 原來(lái)stopCh <- true 停止指令,改為context.WithCancel函數(shù)返回的取消函數(shù)stop()

可以看到函筋,這個(gè)修改之前的代碼結(jié)構(gòu)一樣沙合,只不過(guò)從channel換成了Context。上述示例只是Context的一種使用場(chǎng)景跌帐,它的能力不止于此首懈。

什么是Context

一個(gè)任務(wù)會(huì)有很多協(xié)程協(xié)作完成绊率,一次HTTP請(qǐng)求會(huì)觸發(fā)很多協(xié)程啟動(dòng),而這些協(xié)程有可能會(huì)啟動(dòng)更多子協(xié)程究履,并且無(wú)法預(yù)知有多少層協(xié)程滤否、每一層有多少個(gè)協(xié)程。如果因?yàn)槟承┰驅(qū)е氯蝿?wù)終止了挎袜,HTTP請(qǐng)求取消了顽聂,那么他們啟動(dòng)的協(xié)程怎么辦肥惭?該如何取消呢盯仪?因?yàn)槿∠@些協(xié)程可以節(jié)約內(nèi)存,提升性能蜜葱,同時(shí)避免了不可預(yù)料的Bug全景。

Context就是用來(lái)簡(jiǎn)化這些問(wèn)題的,并且是并發(fā)安全的牵囤。Context是一個(gè)接口爸黄,它具備手動(dòng)、定時(shí)揭鳞、超時(shí)發(fā)出取消信號(hào)炕贵、傳值等功能,主要用于控制多個(gè)協(xié)程之間的協(xié)作野崇,尤其是取消操作称开。一旦取消指令下達(dá),那么被Context跟蹤的協(xié)程都會(huì)受到取消信號(hào)乓梨,可以做清理和退出操作鳖轰。

Context接口只有四個(gè)方法:
type Context interface {
    Deadline() (deadline time.Time, ok bool)
    Done() <- chan struct{}
    Err() error
    Value(ke interface{}) interface{}
}
  1. Deadline 方法可以獲取設(shè)置的截止時(shí)間,第一個(gè)返回值deadline是截止時(shí)間扶镀,到了這個(gè)時(shí)間蕴侣,Context會(huì)自動(dòng)發(fā)起取消請(qǐng)求,第二個(gè)值ok代表是否設(shè)置了截止時(shí)間
  2. Done 方法返回一個(gè)只讀的channel臭觉,類型為struct{}昆雀。在協(xié)程中,如果該方法返回的chan可以讀取蝠筑,則意味著Context已經(jīng)發(fā)起了取消信號(hào)狞膘。通過(guò)Done方法接收到這個(gè)信號(hào)后,就可以做清理操作菱肖,然后退出協(xié)程客冈,釋放資源
  3. Err方法返回取消的錯(cuò)誤原因,即因?yàn)槭裁丛駽ontext被取消
  4. Value方法獲取該Context上綁定的值稳强,是一個(gè)鍵值對(duì)场仲,所以要通過(guò)一個(gè)key才能獲取對(duì)應(yīng)的值

Context接口的四個(gè)方法中最常用的就是Done方法和悦,它返回一個(gè)只讀的channel,用于接收取消信號(hào)渠缕。當(dāng)Context取消的時(shí)候鸽素,會(huì)關(guān)閉這個(gè)只讀channel,也就等于發(fā)出了取消信號(hào)亦鳞。

Context樹(shù)

我們不需要自己實(shí)現(xiàn)Context接口馍忽,Go語(yǔ)言提供了函數(shù)可以幫我們生成不同的Context,通過(guò)這些函數(shù)可以生成一顆Context樹(shù)燕差,這樣Context可以關(guān)聯(lián)起來(lái)遭笋,父Context發(fā)出取消信號(hào)的時(shí)候,子Context也會(huì)發(fā)出徒探,這樣就能控制不同層級(jí)的協(xié)程退出瓦呼。從使用功能上分,有四種實(shí)現(xiàn)好的Context:
  • 空Context:不可取消测暗,沒(méi)有截止時(shí)間央串,主要用于Context樹(shù)根節(jié)點(diǎn)
  • 可取消的Context:用于發(fā)出取消信號(hào),當(dāng)取消的時(shí)候碗啄,它的子Context也會(huì)取消
  • 可定時(shí)取消的Context:多了一個(gè)定時(shí)的功能
  • 值Context:用于存儲(chǔ)一個(gè)key-value鍵值對(duì)
image-20211215165624419

有了根節(jié)點(diǎn)Context后质和,這顆Context樹(shù)如何生成? 需要使用Go語(yǔ)言提供的四個(gè)函數(shù):

  1. WithCancel(parent Contenxt): 生成一個(gè)可取消的Context
  2. WithDeadline(parent Context, d timt.Time):生成一個(gè)可以定是取消的Context稚字,參數(shù)d為定時(shí)取消的具體時(shí)間
  3. WithTimeout(parent Context, timeout time.Duration):生成一個(gè)可超時(shí)取消的Context饲宿,參數(shù)timeout用于設(shè)置多久后取消
  4. WithValue(parent Context, key, val interface{}) :生成一個(gè)可攜帶key-value鍵值對(duì)的Context

上述四個(gè)函數(shù)中,前三個(gè)都屬于可取消的Context尉共,他們是一類函數(shù)褒傅,最后一個(gè)是值Context,用于存儲(chǔ)一個(gè)key-value鍵值對(duì)袄友。

使用Context取消多個(gè)協(xié)程

取消多個(gè)協(xié)程也比較簡(jiǎn)單殿托,把Context作為參數(shù)傳遞給協(xié)程即可。
image-20211215170228020

當(dāng)節(jié)點(diǎn)Ctx2取消時(shí)剧蚣,它的子節(jié)點(diǎn)Ctx4支竹、Ctx6都會(huì)被取消,如果還有子節(jié)點(diǎn)的子節(jié)點(diǎn)鸠按,也會(huì)被取消礼搁。其他節(jié)點(diǎn)不受影響。

Context傳值

Context不僅可以取消目尖,還可以傳值馒吴,通過(guò)這個(gè)能力,可以把Context存儲(chǔ)的值供其他協(xié)程使用。
func main() {
    wg.Add(4)
    
    valCtx := context.WithValue(ctx, "userid", 3)
    go func(){
        defer wg.Done()
        getUser(valCtx)
    }()
}

func getUser(ctx context.Context) {
    for {
        select {
            case <- ctx.Done():
                fmt.Println("協(xié)程退出")
                return 
            default :
            userId := ctx.Value("userid")
            fmt.Println("用戶ID為:"饮戳, userId)
            time.Sleep(2 * time.Second)
        }
    }
}

Context使用原則

Context是一種非常好用的工具豪治,使用它可以很方便的控制取消多個(gè)協(xié)程。在Go語(yǔ)言標(biāo)準(zhǔn)庫(kù)中也使用了它們扯罐,比如net/http中使用Context取消網(wǎng)絡(luò)請(qǐng)求负拟。要更好的使用Context,有一些原則需要盡可能的遵守:
  • Context不要放在結(jié)構(gòu)體中歹河,要以參數(shù)的方式傳遞
  • Context 作為函數(shù)參數(shù)時(shí)掩浙,要放在第一位,即第一個(gè)參數(shù)
  • 要使用context.Background函數(shù)生成根節(jié)點(diǎn)的Context秸歧,也就是最頂層Context
  • Context 傳值要傳必須的值厨姚,盡可能的少,不要什么都傳
  • Context 多協(xié)程安全寥茫,可以在多個(gè)協(xié)程中放心使用

這就是規(guī)范類的遣蚀,Go語(yǔ)言的編譯器不會(huì)做這些檢查,要靠自己遵守纱耻。

如何通過(guò)Context實(shí)現(xiàn)日志跟蹤?

要想跟蹤一個(gè)用戶請(qǐng)求险耀,必須有一個(gè)唯一的ID來(lái)標(biāo)識(shí)這次請(qǐng)求調(diào)用了哪些函數(shù)弄喘、執(zhí)行了哪些代碼,然后通過(guò)這個(gè)唯一ID把日志信息串聯(lián)起來(lái)甩牺。這樣就形成了一個(gè)日志軌跡蘑志,也就實(shí)現(xiàn)了用戶的跟蹤。
  1. 在用戶請(qǐng)求的入口點(diǎn)生成TraceID
  2. 通過(guò)context.WithValue保存TraceID
  3. 然后這個(gè)保存著TraceID的Context作為參數(shù)在各個(gè)協(xié)程或函數(shù)間傳遞
  4. 在需要記錄日志的地方贬派,通過(guò)Context的Value方法獲取保存的TraceID急但,然后把它和其他日志信息記錄下來(lái)
  5. 這樣具備同樣TraceID的日志就可以串聯(lián)起來(lái),達(dá)到日志跟蹤的目的

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末搞乏,一起剝皮案震驚了整個(gè)濱河市波桩,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌请敦,老刑警劉巖镐躲,帶你破解...
    沈念sama閱讀 219,039評(píng)論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異侍筛,居然都是意外死亡萤皂,警方通過(guò)查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,426評(píng)論 3 395
  • 文/潘曉璐 我一進(jìn)店門匣椰,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)裆熙,“玉大人,你說(shuō)我怎么就攤上這事∪肼迹” “怎么了齐媒?”我有些...
    開(kāi)封第一講書(shū)人閱讀 165,417評(píng)論 0 356
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)纷跛。 經(jīng)常有香客問(wèn)我喻括,道長(zhǎng),這世上最難降的妖魔是什么贫奠? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 58,868評(píng)論 1 295
  • 正文 為了忘掉前任唬血,我火速辦了婚禮,結(jié)果婚禮上唤崭,老公的妹妹穿的比我還像新娘拷恨。我一直安慰自己,他們只是感情好谢肾,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,892評(píng)論 6 392
  • 文/花漫 我一把揭開(kāi)白布腕侄。 她就那樣靜靜地躺著,像睡著了一般芦疏。 火紅的嫁衣襯著肌膚如雪冕杠。 梳的紋絲不亂的頭發(fā)上,一...
    開(kāi)封第一講書(shū)人閱讀 51,692評(píng)論 1 305
  • 那天酸茴,我揣著相機(jī)與錄音分预,去河邊找鬼。 笑死薪捍,一個(gè)胖子當(dāng)著我的面吹牛笼痹,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播酪穿,決...
    沈念sama閱讀 40,416評(píng)論 3 419
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼凳干,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來(lái)了被济?” 一聲冷哼從身側(cè)響起救赐,我...
    開(kāi)封第一講書(shū)人閱讀 39,326評(píng)論 0 276
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎溉潭,沒(méi)想到半個(gè)月后净响,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,782評(píng)論 1 316
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡喳瓣,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,957評(píng)論 3 337
  • 正文 我和宋清朗相戀三年馋贤,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片畏陕。...
    茶點(diǎn)故事閱讀 40,102評(píng)論 1 350
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡配乓,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情犹芹,我是刑警寧澤崎页,帶...
    沈念sama閱讀 35,790評(píng)論 5 346
  • 正文 年R本政府宣布,位于F島的核電站腰埂,受9級(jí)特大地震影響飒焦,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜屿笼,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,442評(píng)論 3 331
  • 文/蒙蒙 一牺荠、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧驴一,春花似錦休雌、人聲如沸。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 31,996評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至胸懈,卻和暖如春担扑,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背箫荡。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 33,113評(píng)論 1 272
  • 我被黑心中介騙來(lái)泰國(guó)打工魁亦, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人羔挡。 一個(gè)月前我還...
    沈念sama閱讀 48,332評(píng)論 3 373
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像间唉,于是被迫代替她去往敵國(guó)和親绞灼。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,044評(píng)論 2 355

推薦閱讀更多精彩內(nèi)容