剖析Golang Context:從使用場景到源碼分析

{"type":"doc","content":[{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"前言"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"因爲goroutine,go的併發非常方便,但是這也帶來了另外一個問題,當我們進行一個耗時的異步操作時,如何在約定的時間內終止該操作並返回一個自定義的結果?這也是大家常說的我們如何去終止一個goroutine(因爲goroutine不同於os線程,沒有主動interrupt機制),這裏就輪到今天的主角context登場了。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"context源於google,於1.7版本加入標準庫,按照官方文檔的說法,它是一個請求的全局上下文,攜帶了截止時間、手動取消等信號,幷包含一個併發安全的map用於攜帶數據。context的API比較簡單,標準庫實現上也比較乾淨、獨立,接下來我會從具體的使用場景和源碼分析兩個角度進行闡述。"}]},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"使用技巧"}]},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"使用場景一: 請求鏈路傳值"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"一般來說,我們的根context會在請求的入口處構造如下"}]},{"type":"codeblock","attrs":{"lang":""},"content":[{"type":"text","text":"ctx := context.Background()"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"如果拿捏不準是否需要一個全局的context,可以使用下面這個函數構造"}]},{"type":"codeblock","attrs":{"lang":""},"content":[{"type":"text","text":"ctx := context.TODO()"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong"}],"text":"但是不可以爲nil"},{"type":"text","text":"。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"傳值使用方式如下"}]},{"type":"codeblock","attrs":{"lang":"golang"},"content":[{"type":"text","text":"package main\n\nimport (\n\t\"context\"\n\t\"fmt\"\n)\n\nfunc func1(ctx context.Context) {\n\tctx = context.WithValue(ctx, \"k1\", \"v1\")\n\tfunc2(ctx)\n}\nfunc func2(ctx context.Context) {\n\tfmt.Println(ctx.Value(\"k1\").(string))\n}\n\nfunc main() {\n\tctx := context.Background()\n\tfunc1(ctx)\n}"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"我們在func1通過WithValue(parent Context, key, val interface{}) Context,賦值k1爲v1,在其下層函數func2通過ctx.Value(key interface{}) interface{}獲取k1的值,比較簡單。這裏有個疑問,如果我是在func2裏賦值,在func1裏面能夠拿到這個值嗎?答案是不能,context只能自上而下攜帶值,這個是要注意的一點。"}]},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"使用場景二: 取消耗時操作,及時釋放資源"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"可以考慮這樣一個問題,如果沒有context包,我們如何取消一個耗時操作呢?我這裏模擬了兩種寫法:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"bulletedlist","content":[{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"網絡交互場景,經常通過SetReadDeadline、SetWriteDeadline、SetDeadline進行超時取消"}]}]}]},{"type":"codeblock","attrs":{"lang":"golang"},"content":[{"type":"text","text":"timeout := 10 * time.Second\nt = time.Now().Add(timeout)\nconn.SetDeadline(t)"}]},{"type":"bulletedlist","content":[{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"耗時操作場景,通過select模擬"}]}]}]},{"type":"codeblock","attrs":{"lang":"golang"},"content":[{"type":"text","text":"package main\n\nimport (\n\t\"errors\"\n\t\"fmt\"\n\t\"time\"\n)\n\nfunc func1() error {\n\trespC := make(chan int)\n\t// 處理邏輯\n\tgo func() {\n\t\ttime.Sleep(time.Second * 3)\n\t\trespC child映射關係(cancelCtx、timerCtx這兩種類型會建立,valueCtx類型會一直向上尋找,而循環往上找是因爲cancel是必須的,然後找一種最合理的),這裏children的key是canceler接口,並不能處理所有的外部類型,所以會有else,示例見上述代碼註釋處。對於其他外部類型,不建立直接的傳遞關係。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"parentCancelCtx定義如下:"}]},{"type":"codeblock","attrs":{"lang":"golang"},"content":[{"type":"text","text":"func parentCancelCtx(parent Context) (*cancelCtx, bool) {\n\tfor {\n\t\tswitch c := parent.(type) {\n\t\tcase *cancelCtx:\n\t\t\treturn c, true\n\t\tcase *timerCtx:\n\t\t\treturn &c.cancelCtx, true\n\t\tcase *valueCtx:\n\t\t\tparent = c.Context // 循環往上尋找\n\t\tdefault:\n\t\t\treturn nil, false\n\t\t}\n\t}\n}"}]},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"context是如何觸發取消的"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"上文在闡述傳遞性時的實現時,也包含了一部分取消機制的代碼,這裏不會再列出源碼,但是會依據上述源碼進行說明。對於幾種context,傳遞過程大同小異,但是取消機制有所不同,針對每種類型,我會一一解釋。不同類型的context可以在一條鏈路進行取消,但是每一個context的取消只會被一種條件觸發,所以下面會單獨介紹下每一種context的取消機制(組合取消的場景,按照先到先得的原則,無論那種條件觸發的,都會傳遞調用cancel)。這裏有兩個設計很關鍵:"}]},{"type":"numberedlist","attrs":{"start":"1","normalizeStart":1},"content":[{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":1,"align":null,"origin":null},"content":[{"type":"text","text":" cancel函數是冪等的,可以被多次調用。"}]}]},{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":2,"align":null,"origin":null},"content":[{"type":"text","text":" context中包含done channel可以用來確認是否取消、通知取消。"}]}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"bulletedlist","content":[{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":" "},{"type":"text","marks":[{"type":"strong"}],"text":"cancelCtx類型"}]}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"cancelCtx會主動進行取消,在自頂向下取消的過程中,會遍歷children context,然後依次主動取消。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"cancel函數定義如下"}]},{"type":"codeblock","attrs":{"lang":"golang"},"content":[{"type":"text","text":"func (c *cancelCtx) cancel(removeFromParent bool, err error) {\n\tif err == nil {\n\t\tpanic(\"context: internal error: missing cancel error\")\n\t}\n\tc.mu.Lock()\n\tif c.err != nil {\n\t\tc.mu.Unlock()\n\t\treturn // already canceled\n\t}\n\tc.err = err\n\tif c.done == nil {\n\t\tc.done = closedchan\n\t} else {\n\t\tclose(c.done)\n\t}\n\tfor child := range c.children {\n\t\t// NOTE: acquiring the child's lock while holding parent's lock.\n\t\tchild.cancel(false, err)\n\t}\n\tc.children = nil\n\tc.mu.Unlock()\n\n\tif removeFromParent {\n\t\tremoveChild(c.Context, c)\n\t}\n}"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":" "}]},{"type":"bulletedlist","content":[{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong"}],"text":"timerCtx類型"}]}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"WithTimeout是通過WithDeadline來實現的,均對應timerCtx類型。通過parentCancelCtx函數的定義我們知道,timerCtx也會記錄父子context關係。但是timerCtx是通過timer定時器觸發cancel調用的,部分實現如下"}]},{"type":"codeblock","attrs":{"lang":"golang"},"content":[{"type":"text","text":"\tif c.err == nil {\n\t c.timer = time.AfterFunc(dur, func() {\n\t c.cancel(true, DeadlineExceeded)\n })\n\t}"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"bulletedlist","content":[{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong"}],"text":"靜默包含context"}]}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"這裏暫時只想到了靜默包含即type A struct{context.Context}的情況。通過parentCancelCtx和propagateCancel我們知道這種context不會建立父子context的直接聯繫,但是會通過單獨的goroutine去檢測done channel,來確定是否需要觸發鏈路上的cancel函數,實現見propagateCancel的else部分。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"結尾"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"context的使用注意大致有以下三點:"}]},{"type":"bulletedlist","content":[{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"context只能自頂向下傳值,反之則不可以。"}]}]},{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"如果有cancel,一定要保證調用,否則會造成資源泄露,比如timer泄露。"}]}]},{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"context一定不能爲nil,如果不確定,可以使用context.TODO()生成一個empty的context。"}]}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"context的實現並不複雜,但在實際使用中能給大家帶來不小的便利。使用場景部分通過介紹幾個常見的開發場景使大家對號入座,熟練地使用context;源碼分析部分,通過了解context的實現,能夠在context使用中更加得心應手,做到知其然知其所以然。"}]},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"參考資料"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"link","attrs":{"href":"https://golang.org/pkg/context","title":""},"content":[{"type":"text","text":"golang官方包"}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"link","attrs":{"href":"https://blog.golang.org/context","title":""},"content":[{"type":"text","text":"Go Concurrency Patterns: Context"}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"link","attrs":{"href":"https://github.com/etcd-io/etcd/blob/master/client/client.go#L543","title":""},"content":[{"type":"text","text":"etcd客戶端超時處理示例代碼"}]}]}]}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章