協(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):
- watchDog函數(shù)的stopCh 參數(shù)換成了ctx崭庸,類型為context.Context
- 原來(lái)case <- stopCh 改為 ctx.Done()怀浆,用于判斷是否停止
- 使用context.WithCancel(context.Background())函數(shù)生成一個(gè)可以取消的Context,用于發(fā)送停止指令怕享。這里的context.Background()用于生成一個(gè)空的Context执赡,一般作為整個(gè)Context樹(shù)的根節(jié)點(diǎn)
- 原來(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{}
}
- Deadline 方法可以獲取設(shè)置的截止時(shí)間,第一個(gè)返回值deadline是截止時(shí)間扶镀,到了這個(gè)時(shí)間蕴侣,Context會(huì)自動(dòng)發(fā)起取消請(qǐng)求,第二個(gè)值ok代表是否設(shè)置了截止時(shí)間
- Done 方法返回一個(gè)只讀的channel臭觉,類型為struct{}昆雀。在協(xié)程中,如果該方法返回的chan可以讀取蝠筑,則意味著Context已經(jīng)發(fā)起了取消信號(hào)狞膘。通過(guò)Done方法接收到這個(gè)信號(hào)后,就可以做清理操作菱肖,然后退出協(xié)程客冈,釋放資源
- Err方法返回取消的錯(cuò)誤原因,即因?yàn)槭裁丛駽ontext被取消
- 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ì)
有了根節(jié)點(diǎn)Context后质和,這顆Context樹(shù)如何生成? 需要使用Go語(yǔ)言提供的四個(gè)函數(shù):
- WithCancel(parent Contenxt): 生成一個(gè)可取消的Context
- WithDeadline(parent Context, d timt.Time):生成一個(gè)可以定是取消的Context稚字,參數(shù)d為定時(shí)取消的具體時(shí)間
- WithTimeout(parent Context, timeout time.Duration):生成一個(gè)可超時(shí)取消的Context饲宿,參數(shù)timeout用于設(shè)置多久后取消
- 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é)程即可。
當(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)了用戶的跟蹤。
- 在用戶請(qǐng)求的入口點(diǎn)生成TraceID
- 通過(guò)context.WithValue保存TraceID
- 然后這個(gè)保存著TraceID的Context作為參數(shù)在各個(gè)協(xié)程或函數(shù)間傳遞
- 在需要記錄日志的地方贬派,通過(guò)Context的Value方法獲取保存的TraceID急但,然后把它和其他日志信息記錄下來(lái)
- 這樣具備同樣TraceID的日志就可以串聯(lián)起來(lái),達(dá)到日志跟蹤的目的