為什么需要context
在并發(fā)程序中吧寺,由于超時(shí)论泛、取消操作或者一些異常情況骇钦,往往需要進(jìn)行搶占操作或者中斷后續(xù)操作辩撑。熟悉channel的應(yīng)該都見(jiàn)過(guò)使用done channel來(lái)處理此類(lèi)問(wèn)題界斜。比如以下這個(gè)例子:
func?main()?{????messages?:=?make(chan?int,?10)????
done?:=?make(chan?bool)????
defer?close(messages)???
?//?consumer????go?func()?{????????ticker?:=?time.NewTicker(1?*?time.Second)????????for?_?=?range?ticker.C?{????????????select?{????????????case?<-done:???????????????? ?
?fmt.Println("child?process?interrupt...")????????????????
return????????????
default:????????????????
fmt.Printf("send?message:?%d\n",?<-messages)????????????}????????}????}()??
??//?producer????for?i?:=?0;?i?<?10;?i++?{????????messages?<-?i????}???
?time.Sleep(5?*?time.Second)????
close(done)????time.Sleep(1?*?time.Second)????
fmt.Println("main?process?exit!")}
上述例子中定義了一個(gè)buffer為0的channel done, 子協(xié)程運(yùn)行著定時(shí)任務(wù)。如果主協(xié)程需要在某個(gè)時(shí)刻發(fā)送消息通知子協(xié)程中斷任務(wù)退出合冀,那么就可以讓子協(xié)程監(jiān)聽(tīng)這個(gè)done channel各薇,一旦主協(xié)程關(guān)閉done channel,那么子協(xié)程就可以推出了君躺,這樣就實(shí)現(xiàn)了主協(xié)程通知子協(xié)程的需求峭判。這很好,但是這也是有限的棕叫。
如果我們可以在簡(jiǎn)單的通知上附加傳遞額外的信息來(lái)控制取消:為什么取消林螃,或者有一個(gè)它必須要完成的最終期限,更或者有多個(gè)取消選項(xiàng)俺泣,我們需要根據(jù)額外的信息來(lái)判斷選擇執(zhí)行哪個(gè)取消選項(xiàng)疗认。
考慮下面這種情況:假如主協(xié)程中有多個(gè)任務(wù)1, 2, …m,主協(xié)程對(duì)這些任務(wù)有超時(shí)控制伏钠;而其中任務(wù)1又有多個(gè)子任務(wù)1, 2, …n侮邀,任務(wù)1對(duì)這些子任務(wù)也有自己的超時(shí)控制,那么這些子任務(wù)既要感知主協(xié)程的取消信號(hào)贝润,也需要感知任務(wù)1的取消信號(hào)绊茧。
如果還是使用done channel的用法,我們需要定義兩個(gè)done channel打掘,子任務(wù)們需要同時(shí)監(jiān)聽(tīng)這兩個(gè)done channel华畏。嗯,這樣其實(shí)好像也還行哈尊蚁。但是如果層級(jí)更深亡笑,如果這些子任務(wù)還有子任務(wù),那么使用done channel的方式將會(huì)變得非常繁瑣且混亂横朋。
我們需要一種優(yōu)雅的方案來(lái)實(shí)現(xiàn)這樣一種機(jī)制:
上層任務(wù)取消后仑乌,所有的下層任務(wù)都會(huì)被取消;
中間某一層的任務(wù)取消后,只會(huì)將當(dāng)前任務(wù)的下層任務(wù)取消晰甚,而不會(huì)影響上層的任務(wù)以及同級(jí)任務(wù)衙传。
這個(gè)時(shí)候context就派上用場(chǎng)了。我們首先看看context的結(jié)構(gòu)設(shè)計(jì)和實(shí)現(xiàn)原理厕九。
context是什么
context接口
先看Context接口結(jié)構(gòu)蓖捶,看起來(lái)非常簡(jiǎn)單。
type?Context?interface?{??
??Deadline()?(deadline?time.Time,?ok?bool)??
??Done()?<-chan?struct{}???
?Err()?error????
Value(key?interface{})?interface{}}
Context接口包含四個(gè)方法:
Deadline返回綁定當(dāng)前context的任務(wù)被取消的截止時(shí)間扁远;如果沒(méi)有設(shè)定期限俊鱼,將返回ok == false。
Done當(dāng)綁定當(dāng)前context的任務(wù)被取消時(shí)畅买,將返回一個(gè)關(guān)閉的channel并闲;如果當(dāng)前context不會(huì)被取消,將返回nil谷羞。
Err如果Done返回的channel沒(méi)有關(guān)閉帝火,將返回nil;如果Done返回的channel已經(jīng)關(guān)閉,將返回非空的值表示任務(wù)結(jié)束的原因洒宝。如果是context被取消购公,Err將返回Canceled;如果是context超時(shí)雁歌,Err將返回DeadlineExceeded宏浩。
Value返回context存儲(chǔ)的鍵值對(duì)中當(dāng)前key對(duì)應(yīng)的值,如果沒(méi)有對(duì)應(yīng)的key,則返回nil靠瞎。
可以看到Done方法返回的channel正是用來(lái)傳遞結(jié)束信號(hào)以搶占并中斷當(dāng)前任務(wù)比庄;Deadline方法指示一段時(shí)間后當(dāng)前goroutine是否會(huì)被取消;以及一個(gè)Err方法乏盐,來(lái)解釋goroutine被取消的原因佳窑;而Value則用于獲取特定于當(dāng)前任務(wù)樹(shù)的額外信息。而context所包含的額外信息鍵值對(duì)是如何存儲(chǔ)的呢父能?其實(shí)可以想象一顆樹(shù)神凑,樹(shù)的每個(gè)節(jié)點(diǎn)可能攜帶一組鍵值對(duì),如果當(dāng)前節(jié)點(diǎn)上無(wú)法找到key所對(duì)應(yīng)的值,就會(huì)向上去父節(jié)點(diǎn)里找何吝,直到根節(jié)點(diǎn)溉委,具體后面會(huì)說(shuō)到。
再來(lái)看看context包中的其他關(guān)鍵內(nèi)容爱榕。
emptyCtx
emptyCtx是一個(gè)int類(lèi)型的變量瓣喊,但實(shí)現(xiàn)了context的接口。emptyCtx沒(méi)有超時(shí)時(shí)間黔酥,不能取消藻三,也不能存儲(chǔ)任何額外信息洪橘,所以emptyCtx用來(lái)作為context樹(shù)的根節(jié)點(diǎn)。
//?An?emptyCtx?is?never?canceled,?has?no?values,?and?has?no?deadline.?It?is?not
//?struct{},?since?vars?of?this?type?must?have?distinct?addresses.
type?emptyCtx?intfunc?(*emptyCtx)?Deadline()?(deadline?time.Time,?ok?bool)?{????return}
func?(*emptyCtx)?Done()?<-chan?struct{}?{????return?nil}
func?(*emptyCtx)?Err()?error?{????return?nil}
func?(*emptyCtx)?Value(key?interface{})?interface{}?{????return?nil}
func?(e?*emptyCtx)?String()?string?{????switch?e?{????case?background:????????return?"context.Background"????case?todo:????????return?"context.TODO"????}???
?return?"unknown?empty?Context"}
var?(????background?=?new(emptyCtx)????todo???????=?new(emptyCtx))
func?Background()?Context?{????return?background}
func?TODO()?Context?{????return?todo}
一般不會(huì)直接使用emptyCtx棵帽,而是使用由emptyCtx實(shí)例化的兩個(gè)變量熄求,分別可以通過(guò)調(diào)用Background和TODO方法得到,但這兩個(gè)context在實(shí)現(xiàn)上是一樣的岖寞。那么Background和TODO方法得到的context有什么區(qū)別呢抡四?
Background和TODO只是用于不同場(chǎng)景下:
Background通常被用于主函數(shù)柜蜈、初始化以及測(cè)試中仗谆,作為一個(gè)頂層的context,也就是說(shuō)一般我們創(chuàng)建的context都是基于Background淑履;而TODO是在不確定使用什么context的時(shí)候才會(huì)使用隶垮。
下面將介紹兩種不同功能的基礎(chǔ)context類(lèi)型:valueCtx和cancelCtx。
valueCtx
valueCtx結(jié)構(gòu)體
type?valueCtx?struct?{????Context????key,?val?interface{}}
func?(c?*valueCtx)?Value(key?interface{})?interface{}?{??
??if?c.key?==?key?{????????return?c.val????}???
?return?c.Context.Value(key)}
valueCtx利用一個(gè)Context類(lèi)型的變量來(lái)表示父節(jié)點(diǎn)context秘噪,所以當(dāng)前context繼承了父context的所有信息狸吞;valueCtx類(lèi)型還攜帶一組鍵值對(duì),也就是說(shuō)這種context可以攜帶額外的信息指煎。valueCtx實(shí)現(xiàn)了Value方法蹋偏,用以在context鏈路上獲取key對(duì)應(yīng)的值,如果當(dāng)前context上不存在需要的key,會(huì)沿著context鏈向上尋找key對(duì)應(yīng)的值至壤,直到根節(jié)點(diǎn)威始。
WithValue
WithValue用以向context添加鍵值對(duì):
func?WithValue(parent?Context,?key,?val?interface{})?Context?{??
??if?key?==?nil?{????????panic("nil?key")????}???
?if?!reflect.TypeOf(key).Comparable()?{????????panic("key?is?not?comparable")????}???
?return?&valueCtx{parent,?key,?val}}
這里添加鍵值對(duì)不是在原context結(jié)構(gòu)體上直接添加,而是以此context作為父節(jié)點(diǎn)像街,重新創(chuàng)建一個(gè)新的valueCtx子節(jié)點(diǎn)黎棠,將鍵值對(duì)添加在子節(jié)點(diǎn)上,由此形成一條context鏈镰绎。獲取value的過(guò)程就是在這條context鏈上由尾部上前搜尋:
cancelCtx
cancelCtx結(jié)構(gòu)體
type?cancelCtx?struct?{???
?Context????mu???????sync.Mutex????????????//?protects?following?fields????
?done?????chan?struct{}?????????//?created?lazily,?closed?by?first?cancel?call????
?children?map[canceler]struct{}?//?set?to?nil?by?the?first?cancel?call????
? err??????error?????????????????//?set?to?non-nil?by?the?first?cancel?call}
type?canceler?interface?{????cancel(removeFromParent?bool,?err?error)????
Done()?<-chan?struct{}
}
跟valueCtx類(lèi)似脓斩,cancelCtx中也有一個(gè)context變量作為父節(jié)點(diǎn);變量done表示一個(gè)channel畴栖,用來(lái)表示傳遞關(guān)閉信號(hào)随静;children表示一個(gè)map,存儲(chǔ)了當(dāng)前context節(jié)點(diǎn)下的子節(jié)點(diǎn)吗讶;err用于存儲(chǔ)錯(cuò)誤信息表示任務(wù)結(jié)束的原因燎猛。
再來(lái)看一下cancelCtx實(shí)現(xiàn)的方法:
func?(c?*cancelCtx)?Done()?<-chan?struct{}?{???
?c.mu.Lock()????
if?c.done?==?nil?{????????c.done?=?make(chan?struct{})???
?}????
d?:=?c.done???
?c.mu.Unlock()????
return?d}
func?(c?*cancelCtx)?Err()?error?{???
?c.mu.Lock()????err?:=?c.err????c.mu.Unlock()????return?err}
func?(c?*cancelCtx)?cancel(removeFromParent?bool,?err?error)?{???
?if?err?==?nil?{???????
?panic("context:?internal?error:?missing?cancel?error")????
}????
c.mu.Lock()????if?c.err?!=?nil?{???????
?c.mu.Unlock()????????return?//?already?canceled????
}????//?設(shè)置取消原因???
?c.err?=?err????設(shè)置一個(gè)關(guān)閉的channel或者將done?channel關(guān)閉,用以發(fā)送關(guān)閉信號(hào)???
?if?c.done?==?nil?{???????
?c.done?=?closedchan???
?}?else?{???
?????close(c.done)????}????
//?將子節(jié)點(diǎn)context依次取消????for?child?:=?range?c.children?{???
?????//?NOTE:?acquiring?the?child's?lock
?while?holding?parent's?lock.???????
?child.cancel(false,?err)????}????
c.children?=?nil????
c.mu.Unlock()???
?if?removeFromParent?{????????//?將當(dāng)前context節(jié)點(diǎn)從父節(jié)點(diǎn)上移除???????
?removeChild(c.Context,?c)????}
}
可以發(fā)現(xiàn)cancelCtx類(lèi)型變量其實(shí)也是canceler類(lèi)型关翎,因?yàn)閏ancelCtx實(shí)現(xiàn)了canceler接口扛门。
Done方法和Err方法沒(méi)必要說(shuō)了,cancelCtx類(lèi)型的context在調(diào)用cancel方法時(shí)會(huì)設(shè)置取消原因纵寝,將done channel設(shè)置為一個(gè)關(guān)閉channel或者關(guān)閉channel论寨,然后將子節(jié)點(diǎn)context依次取消星立,如果有需要還會(huì)將當(dāng)前節(jié)點(diǎn)從父節(jié)點(diǎn)上移除。
WithCancel
WithCancel函數(shù)用來(lái)創(chuàng)建一個(gè)可取消的context葬凳,即cancelCtx類(lèi)型的context绰垂。WithCancel返回一個(gè)context和一個(gè)CancelFunc,調(diào)用CancelFunc即可觸發(fā)cancel操作火焰。直接看源碼:
type?CancelFunc?func()
func?WithCancel(parent?Context)?(ctx?Context,?cancel?CancelFunc)?{??
??c?:=?newCancelCtx(parent)????propagateCancel(parent,?&c)????
return?&c,?func()?{?c.cancel(true,?Canceled)?}
}
//?newCancelCtx?returns?an?initialized?cancelCtx.func?newCancelCtx(parent?Context)?cancelCtx?{???
?//?將parent作為父節(jié)點(diǎn)context生成一個(gè)新的子節(jié)點(diǎn)????
return?cancelCtx{Context:?parent}}
func?propagateCancel(parent?Context,?child?canceler)?{????if?parent.Done()?==?nil?{????????
//?parent.Done()返回nil表明父節(jié)點(diǎn)以上的路徑上沒(méi)有可取消的context????????return?//?parent?is?never?canceled????}????//?獲取最近的類(lèi)型為cancelCtx的祖先節(jié)點(diǎn)???
?if?p,?ok?:=?parentCancelCtx(parent);?ok?{????????p.mu.Lock()????????if?p.err?!=?nil?{???????????
?//?parent?has?already?been?canceled????????????child.cancel(false,?p.err)???????
?}?else?{???????????
?if?p.children?==?nil?{????????????????p.children?=?make(map[canceler]struct{})????????????}???????????
?//?將當(dāng)前子節(jié)點(diǎn)加入最近c(diǎn)ancelCtx祖先節(jié)點(diǎn)的children中???????????
?p.children[child]?=?struct{}{}???????
?}????????
p.mu.Unlock()???
?}?else?{????????go?func()?{???????????
?select?{????????????
case?<-parent.Done():???????????????
? ? ?????child.cancel(false,?parent.Err())???????????
?case?<-child.Done():????????????}????????}()????}
}
func?parentCancelCtx(parent?Context)?(*cancelCtx,?bool)?
{????for?{????????switch?c?:=?parent:(type)?{
????????case?*cancelCtx:????????????
????????return?c,?true????????
????????case?*timerCtx:????????????
????????return?&c.cancelCtx,?true? ? ? ? ? ? ? ? ????????case?*valueCtx:????????????
? ? ? ? ?parent?=?c.Context????????default:????????????
????????return?nil,?false????????}???
?}
}
之前說(shuō)到cancelCtx取消時(shí)劲装,會(huì)將后代節(jié)點(diǎn)中所有的cancelCtx都取消,propagateCancel即用來(lái)建立當(dāng)前節(jié)點(diǎn)與祖先節(jié)點(diǎn)這個(gè)取消關(guān)聯(lián)邏輯昌简。
如果parent.Done()返回nil占业,表明父節(jié)點(diǎn)以上的路徑上沒(méi)有可取消的context,不需要處理纯赎;
如果在context鏈上找到到cancelCtx類(lèi)型的祖先節(jié)點(diǎn)谦疾,則判斷這個(gè)祖先節(jié)點(diǎn)是否已經(jīng)取消已维,如果已經(jīng)取消就取消當(dāng)前節(jié)點(diǎn)六孵;否則將當(dāng)前節(jié)點(diǎn)加入到祖先節(jié)點(diǎn)的children列表拷泽。
否則開(kāi)啟一個(gè)協(xié)程趴俘,監(jiān)聽(tīng)parent.Done()和child.Done()哥纫,一旦parent.Done()返回的channel關(guān)閉硼一,即context鏈中某個(gè)祖先節(jié)點(diǎn)context被取消浸卦,則將當(dāng)前context也取消程帕。
這里或許有個(gè)疑問(wèn)该默,為什么是祖先節(jié)點(diǎn)而不是父節(jié)點(diǎn)瞳氓?這是因?yàn)楫?dāng)前context鏈可能是這樣的:
當(dāng)前cancelCtx的父節(jié)點(diǎn)context并不是一個(gè)可取消的context,也就沒(méi)法記錄children权均。
timerCtx
timerCtx是一種基于cancelCtx的context類(lèi)型顿膨,從字面上就能看出,這是一種可以定時(shí)取消的context叽赊。
type?timerCtx?struct?{????
????cancelCtx????timer?*time.Timer?//?Under?cancelCtx.mu.? ? ? ? ?????deadline?time.Time}
????func?(c?*timerCtx)?Deadline()?(deadline?time.Time,?ok?bool)?{?
???return?c.deadline,?true}
func?(c?*timerCtx)?cancel(removeFromParent?bool,?err?error)?{????將內(nèi)部的cancelCtx取消????
c.cancelCtx.cancel(false,?err)????
if?removeFromParent?{????????????
//?Remove?this?timerCtx?from?its?parent?cancelCtx's?children.????????removeChild(c.cancelCtx.Context,?c)????}????
c.mu.Lock()????if?c.timer?!=?nil?{???????
// 取消計(jì)時(shí)器????????
c.timer.Stop()???????
?c.timer?=?nil????
}???
?c.mu.Unlock()
}
timerCtx內(nèi)部使用cancelCtx實(shí)現(xiàn)取消恋沃,另外使用定時(shí)器timer和過(guò)期時(shí)間deadline實(shí)現(xiàn)定時(shí)取消的功能。timerCtx在調(diào)用cancel方法必指,會(huì)先將內(nèi)部的cancelCtx取消囊咏,如果需要?jiǎng)t將自己從cancelCtx祖先節(jié)點(diǎn)上移除,最后取消計(jì)時(shí)器塔橡。
WithDeadline
WithDeadline返回一個(gè)基于parent的可取消的context梅割,并且其過(guò)期時(shí)間deadline不晚于所設(shè)置時(shí)間d。
func?WithDeadline(parent?Context,?d?time.Time)?(Context,?CancelFunc)?{???
?if?cur,?ok?:=?parent.Deadline();?ok?&&?cur.Before(d)?{???????
?//?The?current?deadline?is?already?sooner?than?the?new?one.????????return?WithCancel(parent)????}????
c?:=?&timerCtx{????????cancelCtx:?newCancelCtx(parent),????????deadline:??d,????}????
//?建立新建context與可取消context祖先節(jié)點(diǎn)的取消關(guān)聯(lián)關(guān)系????propagateCancel(parent,?c)???
?????????dur?:=?time.Until(d)????
? ? ? ? ? ?if?dur?<=?0?{????????c.cancel(true,?DeadlineExceeded)?//?deadline?has?already?passed????????
return?c,?
func()?{?c.cancel(false,?Canceled)?}???
?}???
?c.mu.Lock()????
defer?c.mu.Unlock()????if?c.err?==?nil?{????????
c.timer?=?time.AfterFunc(dur,?func()?{????????????c.cancel(true,?DeadlineExceeded)????????})????}????
return?c,?func()?{?c.cancel(true,?Canceled)?}
}
如果父節(jié)點(diǎn)parent有過(guò)期時(shí)間并且過(guò)期時(shí)間早于給定時(shí)間d葛家,那么新建的子節(jié)點(diǎn)context無(wú)需設(shè)置過(guò)期時(shí)間户辞,使用WithCancel創(chuàng)建一個(gè)可取消的context即可;
否則癞谒,就要利用parent和過(guò)期時(shí)間d創(chuàng)建一個(gè)定時(shí)取消的timerCtx底燎,并建立新建context與可取消context祖先節(jié)點(diǎn)的取消關(guān)聯(lián)關(guān)系刃榨,接下來(lái)判斷當(dāng)前時(shí)間距離過(guò)期時(shí)間d的時(shí)長(zhǎng)dur:
如果dur小于0,即當(dāng)前已經(jīng)過(guò)了過(guò)期時(shí)間双仍,則直接取消新建的timerCtx枢希,原因?yàn)镈eadlineExceeded;
否則朱沃,為新建的timerCtx設(shè)置定時(shí)器苞轿,一旦到達(dá)過(guò)期時(shí)間即取消當(dāng)前timerCtx。
WithTimeout
與WithDeadline類(lèi)似逗物,WithTimeout也是創(chuàng)建一個(gè)定時(shí)取消的context搬卒,只不過(guò)WithDeadline是接收一個(gè)過(guò)期時(shí)間點(diǎn),而WithTimeout接收一個(gè)相對(duì)當(dāng)前時(shí)間的過(guò)期時(shí)長(zhǎng)timeout:
func?WithTimeout(parent?Context,?timeout?time.Duration)?(Context,?CancelFunc)?{???
?return?WithDeadline(parent,?time.Now().Add(timeout))
}
context的使用
首先使用context實(shí)現(xiàn)文章開(kāi)頭done channel的例子來(lái)示范一下如何更優(yōu)雅實(shí)現(xiàn)協(xié)程間取消信號(hào)的同步:
func?main()?{????
messages?:=?make(chan?int,?10)????//?producer????
for?i?:=?0;?i?<?10;?i++?{????????messages?<-?i????}???
?ctx,?cancel?:=?context.WithTimeout(context.Background(),?5*time.Second)????
//?consumer????
go?func(ctx?context.Context)?{????????ticker?:=?time.NewTicker(1?*?time.Second)????????
for?_?=?range?ticker.C?{????????????select?{????????????case?<-ctx.Done():????????????????fmt.Println("child?process?interrupt...")????????????????return????????????default:???????????????
?fmt.Printf("send?message:?%d\n",?<-messages)????????????}???????
?}???
?}(ctx)????
defer?close(messages)????
defer?cancel()????select?{????
case?<-ctx.Done():????????
time.Sleep(1?*?time.Second)????????fmt.Println("main?process?exit!")????}
}
這個(gè)例子中敬察,只要讓子線(xiàn)程監(jiān)聽(tīng)主線(xiàn)程傳入的ctx秀睛,一旦ctx.Done()返回空channel尔当,子線(xiàn)程即可取消執(zhí)行任務(wù)莲祸。但這個(gè)例子還無(wú)法展現(xiàn)context的傳遞取消信息的強(qiáng)大優(yōu)勢(shì)。
閱讀過(guò)net/http包源碼的朋友可能注意到在實(shí)現(xiàn)http server時(shí)就用到了context, 下面簡(jiǎn)單分析一下椭迎。
1锐帜、首先Server在開(kāi)啟服務(wù)時(shí)會(huì)創(chuàng)建一個(gè)valueCtx,存儲(chǔ)了server的相關(guān)信息,之后每建立一條連接就會(huì)開(kāi)啟一個(gè)協(xié)程畜号,并攜帶此valueCtx缴阎。
func?(srv?*Server)?Serve(l?net.Listener)?error?{?
???...????
var?tempDelay?time.Duration?????//?how?long?to?sleep?on?accept?failure????
baseCtx?:=?context.Background()?//?base?is?always?background,?per?Issue?16220????
ctx?:=?context.WithValue(baseCtx,?ServerContextKey,?srv)????for?{???????
?rw,?e?:=?l.Accept()????????...???????
?tempDelay?=?0????????
c?:=?srv.newConn(rw)???????
?c.setState(c.rwc,?StateNew)?//?before?Serve?can?return????????
go?c.serve(ctx)????}
}
2、建立連接之后會(huì)基于傳入的context創(chuàng)建一個(gè)valueCtx用于存儲(chǔ)本地地址信息简软,之后在此基礎(chǔ)上又創(chuàng)建了一個(gè)cancelCtx蛮拔,然后開(kāi)始從當(dāng)前連接中讀取網(wǎng)絡(luò)請(qǐng)求,每當(dāng)讀取到一個(gè)請(qǐng)求則會(huì)將該cancelCtx傳入痹升,用以傳遞取消信號(hào)建炫。一旦連接斷開(kāi),即可發(fā)送取消信號(hào)疼蛾,取消所有進(jìn)行中的網(wǎng)絡(luò)請(qǐng)求肛跌。
func?(c?*conn)?serve(ctx?context.Context)?{???
?c.remoteAddr?=?c.rwc.RemoteAddr().String()????
ctx?=?context.WithValue(ctx,?LocalAddrContextKey,?c.rwc.LocalAddr())???
?...???
?ctx,cancelCtx?:=?context.WithCancel(ctx)????
c.cancelCtx?=?cancelCtx????defer?cancelCtx()???
?...????
for?{????????w,?err?:=?c.readRequest(ctx)???????
?...???????
?serverHandler{c.server}.ServeHTTP(w,?w.req)???????
?...????}
}
3、讀取到請(qǐng)求之后察郁,會(huì)再次基于傳入的context創(chuàng)建新的cancelCtx,并設(shè)置到當(dāng)前請(qǐng)求對(duì)象req上衍慎,同時(shí)生成的response對(duì)象中cancelCtx保存了當(dāng)前context取消方法。
func?(c?*conn)?readRequest(ctx?context.Context)?(w?*response,?err?error)?{??
??...???
?req,?err?:=?readRequest(c.bufr,?keepHostHeader)????
...????ctx,?cancelCtx?:=?context.WithCancel(ctx)????
req.ctx?=?ctx????
...????
w?=?&response{????????
conn:??????????c,????????
cancelCtx:?????cancelCtx,???????
?req:???????????req,???????
?reqBody:???????req.Body,???????
?handlerHeader:?make(Header),????????
contentLength:?-1,????????
closeNotifyCh:?make(chan?bool,?1),????????
//?We?populate?these?ahead?of?time?so?we're?not????????
//?reading?from?req.Header?after?their?Handler?starts????????
//?and?maybe?mutates?it?(Issue?14940)????????
wants10KeepAlive:?req.wantsHttp10KeepAlive(),???????
?wantsClose:???????req.wantsClose(),????}???
?...???
?return?w,?nil}
這樣處理的目的主要有以下幾點(diǎn):
一旦請(qǐng)求超時(shí)皮钠,即可中斷當(dāng)前請(qǐng)求稳捆;
在處理構(gòu)建response過(guò)程中如果發(fā)生錯(cuò)誤,可直接調(diào)用response對(duì)象的cancelCtx方法結(jié)束當(dāng)前請(qǐng)求麦轰;
在處理構(gòu)建response完成之后乔夯,調(diào)用response對(duì)象的cancelCtx方法結(jié)束當(dāng)前請(qǐng)求期虾。
在整個(gè)server處理流程中,使用了一條context鏈貫穿Server驯嘱、Connection镶苞、Request,不僅將上游的信息共享給下游任務(wù)鞠评,同時(shí)實(shí)現(xiàn)了上游可發(fā)送取消信號(hào)取消所有下游任務(wù)茂蚓,而下游任務(wù)自行取消不會(huì)影響上游任務(wù)。
總結(jié)
context主要用于父子任務(wù)之間的同步取消信號(hào)剃幌,本質(zhì)上是一種協(xié)程調(diào)度的方式聋涨。另外在使用context時(shí)有兩點(diǎn)值得注意:上游任務(wù)僅僅使用context通知下游任務(wù)不再需要,但不會(huì)直接干涉和中斷下游任務(wù)的執(zhí)行负乡,由下游任務(wù)自行決定后續(xù)的處理操作牍白,也就是說(shuō)context的取消操作是無(wú)侵入的;context是線(xiàn)程安全的抖棘,因?yàn)閏ontext本身是不可變的(immutable)茂腥,因此可以放心地在多個(gè)協(xié)程中傳遞使用。