1 什么是Context
最近在公司分析gRPC源碼俊戳,proto文件生成的代碼馆匿,接口函數(shù)第一個參數(shù)統(tǒng)一是ctx context.Context接口,公司不少同事都不了解這樣設(shè)計的出發(fā)點是什么渐北,其實我也不了解其背后的原理。今天趁著妮妲臺風(fēng)妹子正面登陸深圳恃锉,全市停工、停課破托、停業(yè),在家休息找了一些資料研究把玩一把澎嚣。
Context通常被譯作上下文,它是一個比較抽象的概念易桃。在公司技術(shù)討論時也經(jīng)常會提到上下文锌俱。一般理解為程序單元的一個運行狀態(tài)、現(xiàn)場贸宏、快照,而翻譯中上下又很好地詮釋了其本質(zhì)诫龙,上下上下則是存在上下層的傳遞,上會把內(nèi)容傳遞給下签赃。在Go語言中分尸,程序單元也就指的是Goroutine。
每個Goroutine在執(zhí)行之前箩绍,都要先知道程序當(dāng)前的執(zhí)行狀態(tài),通常將這些執(zhí)行狀態(tài)封裝在一個Context變量中圆到,傳遞給要執(zhí)行的Goroutine中卑吭。上下文則幾乎已經(jīng)成為傳遞與請求同生存周期變量的標(biāo)準(zhǔn)方法。在網(wǎng)絡(luò)編程下陨簇,當(dāng)接收到一個網(wǎng)絡(luò)請求Request河绽,處理Request時,我們可能需要開啟不同的Goroutine來獲取數(shù)據(jù)與邏輯處理耙饰,即一個請求Request,會在多個Goroutine中處理廷痘。而這些Goroutine可能需要共享Request的一些信息;同時當(dāng)Request被取消或者超時的時候笋额,所有從這個Request創(chuàng)建的所有Goroutine也應(yīng)該被結(jié)束篷扩。
2 context包
Go的設(shè)計者早考慮多個Goroutine共享數(shù)據(jù),以及多Goroutine管理機制鉴未。Context介紹請參考Go Concurrency Patterns: Context,golang.org/x/net/context包就是這種機制的實現(xiàn)淹真。
context包不僅實現(xiàn)了在程序單元之間共享狀態(tài)變量的方法连茧,同時能通過簡單的方法,使我們在被調(diào)用程序單元的外部梅屉,通過設(shè)置ctx變量值,將過期或撤銷這些信號傳遞給被調(diào)用的程序單元虐唠。在網(wǎng)絡(luò)編程中,若存在A調(diào)用B的API, B再調(diào)用C的API疆偿,若A調(diào)用B取消搓幌,那也要取消B調(diào)用C,通過在A,B,C的API調(diào)用之間傳遞Context溉愁,以及判斷其狀態(tài),就能解決此問題撤蟆,這是為什么gRPC的接口中帶上ctx context.Context參數(shù)的原因之一。
Go1.7(當(dāng)前是RC2版本)已將原來的golang.org/x/net/context包挪入了標(biāo)準(zhǔn)庫中家肯,放在$GOROOT/src/context下面。標(biāo)準(zhǔn)庫中net换棚、net/http、os/exec都用到了context固蚤。同時為了考慮兼容愿险,在原golang.org/x/net/context包下存在兩個文件,go17.go是調(diào)用標(biāo)準(zhǔn)庫的context包辆亏,而pre_go17.go則是之前的默認(rèn)實現(xiàn),其介紹請參考go程序包源碼解讀缤弦。
context包的核心就是Context接口,其定義如下:
type Contextinterface{? ? Deadline() (deadline time.Time, okbool)? ? Done() <-chanstruct{}? ? Err()errorValue(keyinterface{})interface{}}
Deadline會返回一個超時時間碍沐,Goroutine獲得了超時時間后衷蜓,例如可以對某些io操作設(shè)定超時時間。
Done方法返回一個信道(channel)磁浇,當(dāng)Context被撤銷或過期時,該信道是關(guān)閉的无虚,即它是一個表示Context是否已關(guān)閉的信號。
當(dāng)Done信道關(guān)閉后友题,Err方法表明Context被撤的原因戴质。
Value可以讓Goroutine共享一些數(shù)據(jù)踢匣,當(dāng)然獲得數(shù)據(jù)是協(xié)程安全的斗埂。但使用這些數(shù)據(jù)的時候要注意同步,比如返回了一個map,而這個map的讀寫則要加鎖行贪。
Context接口沒有提供方法來設(shè)置其值和過期時間,也沒有提供方法直接將其自身撤銷建瘫。也就是說,Context不能改變和撤銷其自身啰脚。那么該怎么通過Context傳遞改變后的狀態(tài)呢?
3 context使用
無論是Goroutine粒梦,他們的創(chuàng)建和調(diào)用關(guān)系總是像層層調(diào)用進行的荸实,就像人的輩分一樣,而更靠頂部的Goroutine應(yīng)有辦法主動關(guān)閉其下屬的Goroutine的執(zhí)行(不然程序可能就失控了)准给。為了實現(xiàn)這種關(guān)系,Context結(jié)構(gòu)也應(yīng)該像一棵樹露氮,葉子節(jié)點須總是由根節(jié)點衍生出來的。
要創(chuàng)建Context樹局扶,第一步就是要得到根節(jié)點油讯,context.Background函數(shù)的返回值就是根節(jié)點:
funcBackground()Context
該函數(shù)返回空的Context,該Context一般由接收請求的第一個Goroutine創(chuàng)建陌兑,是與進入請求對應(yīng)的Context根節(jié)點,它不能被取消兔综、沒有值狞玛、也沒有過期時間心肪。它常常作為處理Request的頂層context存在。
有了根節(jié)點硬鞍,又該怎么創(chuàng)建其它的子節(jié)點戴已,孫節(jié)點呢?context包為我們提供了多個函數(shù)來創(chuàng)建他們:
funcWithCancel(parent Context)(ctx Context, cancel CancelFunc)funcWithDeadline(parent Context, deadline time.Time)(Context, CancelFunc)funcWithTimeout(parent Context, timeout time.Duration)(Context, CancelFunc)funcWithValue(parent Context, keyinterface{}, valinterface{})Context
函數(shù)都接收一個Context類型的參數(shù)parent糖儡,并返回一個Context類型的值,這樣就層層創(chuàng)建出不同的節(jié)點桦沉。子節(jié)點是從復(fù)制父節(jié)點得到的金闽,并且根據(jù)接收參數(shù)設(shè)定子節(jié)點的一些狀態(tài)值,接著就可以將子節(jié)點傳遞給下層的Goroutine了呐矾。
再回到之前的問題:該怎么通過Context傳遞改變后的狀態(tài)呢?使用Context的Goroutine無法取消某個操作蜒犯,其實這也是符合常理的,因為這些Goroutine是被某個父Goroutine創(chuàng)建的玉工,而理應(yīng)只有父Goroutine可以取消操作淘菩。在父Goroutine中可以通過WithCancel方法獲得一個cancel方法,從而獲得cancel的權(quán)利潮改。
第一個WithCancel函數(shù),它是將父節(jié)點復(fù)制到子節(jié)點汇在,并且還返回一個額外的CancelFunc函數(shù)類型變量,該函數(shù)類型的定義為:
typeCancelFuncfunc()
調(diào)用CancelFunc對象將撤銷對應(yīng)的Context對象亩鬼,這就是主動撤銷Context的方法殖告。在父節(jié)點的Context所對應(yīng)的環(huán)境中黄绩,通過WithCancel函數(shù)不僅可創(chuàng)建子節(jié)點的Context玷过,同時也獲得了該節(jié)點Context的控制權(quán),一旦執(zhí)行該函數(shù)冶匹,則該節(jié)點Context就結(jié)束了,則子節(jié)點需要類似如下代碼來判斷是否已結(jié)束嚼隘,并退出該Goroutine:
select{case<-cxt.Done():? ? ? ? //dosomeclean...}
WithDeadline函數(shù)的作用也差不多飞蛹,它返回的Context類型值同樣是parent的副本灸眼,但其過期時間由deadline和parent的過期時間共同決定。當(dāng)parent的過期時間早于傳入的deadline時間時焰宣,返回的過期時間應(yīng)與parent相同。父節(jié)點過期時匕积,其所有的子孫節(jié)點必須同時關(guān)閉;反之盅粪,返回的父節(jié)點的過期時間則為deadline。
WithTimeout函數(shù)與WithDeadline類似票顾,只不過它傳入的是從現(xiàn)在開始Context剩余的生命時長帆调。他們都同樣也都返回了所創(chuàng)建的子Context的控制權(quán),一個CancelFunc類型的函數(shù)變量番刊。
當(dāng)頂層的Request請求函數(shù)結(jié)束后,我們就可以cancel掉某個context撵枢,從而層層Goroutine根據(jù)判斷cxt.Done()來結(jié)束精居。
WithValue函數(shù)潜必,它返回parent的一個副本,調(diào)用該副本的Value(key)方法將得到val佛吓。這樣我們不光將根節(jié)點原有的值保留了,還在子孫節(jié)點中加入了新的值维雇,注意若存在Key相同晒他,則會被覆蓋。
3.1 小結(jié)
context包通過構(gòu)建樹型關(guān)系的Context陨仅,來達(dá)到上一層Goroutine能對傳遞給下一層Goroutine的控制。對于處理一個Request請求操作灼伤,需要采用context來層層控制Goroutine,以及傳遞一些變量來共享撞鹉。
Context對象的生存周期一般僅為一個請求的處理周期。即針對一個請求創(chuàng)建一個Context變量(它為Context樹結(jié)構(gòu)的根)鸟雏;在請求處理結(jié)束后发皿,撤銷此ctx變量,釋放資源穴墅。
每次創(chuàng)建一個Goroutine,要么將原有的Context傳遞給Goroutine玄货,要么創(chuàng)建一個子Context并傳遞給Goroutine。
Context能靈活地存儲不同類型夹界、不同數(shù)目的值,并且使多個Goroutine安全地讀寫其中的值可柿。
當(dāng)通過父Context對象創(chuàng)建子Context對象時,可同時獲得子Context的一個撤銷函數(shù)复斥,這樣父Context對象的創(chuàng)建環(huán)境就獲得了對子Context將要被傳遞到的Goroutine的撤銷權(quán)。
4 使用原則
Programs that use Contexts should follow these rules to keep interfaces consistent across packages and enable static analysis tools to check context propagation:
使用Context的程序包需要遵循如下的原則來滿足接口的一致性以及便于靜態(tài)分析目锭。
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存在一個結(jié)構(gòu)體當(dāng)中被去,顯式地傳入函數(shù)。Context變量需要作為第一個參數(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饭豹;即使方法允許渊额,也不要傳入一個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相關(guān)方法只應(yī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在多個goroutine中是安全的;
在子Context被傳遞到的goroutine中仔掸,應(yīng)該對該子Context的Done信道(channel)進行監(jiān)控,一旦該信道被關(guān)閉(即上層運行環(huán)境撤銷了本goroutine的執(zhí)行)起暮,應(yīng)主動終止對當(dāng)前請求信息的處理,釋放資源并返回负懦。