1. 程式人生 > 其它 >go並行程式設計4-context

go並行程式設計4-context

context

在 Go 服務中,每個傳入的請求都在其自己的goroutine 中處理。請求處理程式通常啟動額外的 goroutine 來訪問其他後端,如資料庫和 RPC 服務。處理請求的 goroutine 通常需要訪問特定於請求(request-specific context)的值,例如終端使用者的身份、授權令牌和請求的截止日期(deadline)。

當一個請求被取消或超時時,處理該請求的所有 goroutine 都應該快速退出(fail fast),這樣系統就可以回收它們正在使用的任何資源。

Go 1.7 引入一個 context 包,它使得跨 API 邊界的請求範圍元資料、取消訊號和截止日期很容易傳遞給處理請求所涉及的所有 goroutine(顯示傳遞)。

其他語言: Thread Local Storage(TLS),XXXContext

 如何將 context 整合到 API 中? 在將 context 整合到 API 中時,要記住的最重要的一點是,它的作用域是請求級別的。

例如,沿單個數據庫查詢存在是有意義的,但沿資料庫物件存在則沒有意義。 目前有兩種方法可以將 context 物件整合到 API 中:

  1. 首引數傳遞 context 物件,比如,參考 net 包 Dialer.DialContext。此函式執行正常的 Dial 操作,但可以通過 context 物件取消函式呼叫。
  2. 在第一個 request 物件中攜帶一個可選的 context 物件。例如 net/http 庫的 Request.WithContext,通過攜帶給定的 context 物件,返回一個新的 Request 物件。     

 不要在結構型別中儲存context;相反,顯式地將Context傳遞給每個需要它的函式。上下文應該是第一個引數,通常命名為ctx:

 對伺服器的傳入請求應該建立一個Context。

使用 context 的一個很好的心智模型是它應該在程式中流動,應該貫穿你的程式碼。這通常意味著您不希望將其儲存在結構體之中。它從一個函式傳遞到另一個函式,並根據需要進行擴充套件。理想情況下,每個請求都會建立一個 context 物件,並在請求結束時過期。

不儲存上下文的一個例外是,當您需要將它放入一個結構中時,該結構純粹用作通過通道傳遞的訊息。如下例所示。

context.WithValue

context.WithValue 內部基於 valueCtx 實現:

為了實現不斷的 WithValue,構建新的 context,內部在查詢 key 時候,使用遞迴方式不斷從當前,從父節點尋找匹配的 key,直到 root context(Backgrond 和 TODO Value 函式會返回 nil)。

 在上下文中傳遞除錯或跟蹤資料是安全的

context.WithValue 方法允許上下文攜帶請求範圍的資料。這些資料必須是安全的,以便多個 goroutine 同時使用。這裡的資料,更多是面向請求的元資料,不應該作為函式的可選引數來使用(比如 context 裡面掛了一個sql.Tx 物件,傳遞到 data 層使用),因為元資料相對函式引數更加是隱含的,面向請求的。而引數是更加顯示的。

同一個 context 物件可以傳遞給在不同 goroutine 中執行的函式;上下文對於多個 goroutine 同時使用是安全的。對於值型別最容易犯錯的地方,在於 context value 應該是 immutable 的,每次重新賦值應該是新的 context,即: context.WithValue(ctx, oldvalue) https://pkg.go.dev/google.golang.org/grpc/metadata Context.Value should inform, not control

 比如我們新建了一個基於 context.Background() 的 ctx1,攜帶了一個 map 的資料,map 中包含了 “k1”: “v1” 的一個鍵值對,ctx1 被兩個 goroutine 同時使用作為函式簽名傳入,如果我們修改了 這個map,會導致另外進行讀 context.Value 的 goroutine 和修改 map 的 goroutine,在 map 物件上產生 data race。因此我們要使用 copy-on-write 的思路,解決跨多個 goroutine 使用資料、修改資料的場景。

 從 ctx1 中獲取 map1(可以理解為 v1 版本的 map 資料)。構建一個新的 map 物件 map2,複製所有 map1 資料,同時追加新的資料 “k2”: “v2” 鍵值對,使用 context.WithValue 建立新的 ctx2,ctx2 會傳遞到其他的 goroutine 中。這樣各自讀取的副本都是自己的資料,寫行為追加的資料,在 ctx2 中也能完整讀取到,同時也不會汙染 ctx1 中的資料。

 當一個上下文被取消時,從它派生的所有上下文也被取消。

當一個 context 被取消時,從它派生的所有 context 也將被取消。WithCancel(ctx) 引數 ctx 認為是 parent ctx,在內部會進行一個傳播關係鏈的關聯。Done() 返回 一個 chan,當我們取消某個parent context, 實際上上會遞迴層層 cancel 掉自己的 child context 的 done chan 從而讓整個呼叫鏈中所有監聽 cancel 的 goroutine 退出。

如果要實現一個超時控制,通過上面的 context 的 parent/child 機制,其實我們只需要啟動一個定時器,然後在超時的時候,直接將當前的 context 給 cancel 掉,就可以實現監聽在當前和下層的額 context.Done() 的 goroutine 的退出。

 總結:

  1. 對伺服器的傳入請求應該建立一個Context。
  2. 向伺服器發出的呼叫應該接受一個Context。
  3. 不要在結構型別中儲存context;相反,顯式地將Context傳遞給每個需要它的函式。
  4. 它們之間的函式呼叫鏈必須傳播上下文。
  5. 使用WithCancel、WithDeadline、WithTimeout或WithValue替換上下文。
  6. 當一個上下文被取消時,從它派生的所有上下文也被取消。
  7. 相同的上下文可以傳遞給執行在不同goroutine中的函式;上下文對於多個goroutine同時使用是安全的。
  8. 不要傳遞空上下文,即使函式允許這樣做。如果您不確定要使用哪個上下文,則傳遞一個TODO上下文。
  9. 上下文值只用於傳遞程序和api的請求範圍內的資料,而不是傳遞可選引數給函式。
  10. 所有阻塞/長時間操作都應該是可取消的。
  11. 上下文。值模糊了程式的流。
  12. 上下文。價值應該告知,而不是控制。
  13. 儘量不要使用context.Value。