Go語言的context

Golang context 實現原理

本篇文章是基于小徐先生的文章的修改和個人注解,要查看原文可以點擊上述的鏈接查看

目前我這篇文章的go語言版本是1.24.1

context上下文

context被當作第一個參數(官方建議),并且不斷的傳遞下去,基本上一個項目代碼到處都是context,但是你們真的知道他有什么作用嗎?

接下來就來看看這個golang世界中的典型工具吧

func main()  {ctx,cancel := context.WithTimeout(context.Background(),10 * time.Second)defer cancel()go Monitor(ctx)time.Sleep(20 * time.Second)
}func Monitor(ctx context.Context)  {for {fmt.Print("monitor")}
}

但是他到底時如何處理并發控制和實現呢?

接下來就來深入看看他的原理和使用

一.context包介紹

context可以用來在goroutine之間傳遞上下文信息,相同的context可以傳遞給運行在不同goroutine中的函數,上下文對于多個goroutine同時使用是安全的,context包定義了上下文類型,可以使用backgroundTODO創建一個上下文,在函數調用鏈之間傳播context,也可以使用WithDeadlineWithTimeoutWithCancelWithValue 創建的修改副本替換它,聽起來有點繞,其實總結起就是一句話:context的作用就是在不同的goroutine之間同步請求特定的數據、取消信號以及處理請求的截止日期。

目前我們常用的一些庫都是支持context的,例如gindatabase/sql等庫都是支持context的,這樣更方便我們做并發控制了,只要在服務器入口創建一個context上下文,不斷透傳下去即可。

二.context的使用

2.1 context.Context

他是核心數據結構,看一下它的樣子吧:

type Context interface {Deadline() (deadline time.Time, ok bool)Done() <-chan struct{}Err() errorValue(key any) any
}

Context 為 interface,定義了四個核心 api:

? Deadline:返回 context 的過期時間

? Done:返回 context 中的 channel

? Err:返回錯誤

? Value:返回 context 中的對應 key 的值

2.2 標準error

var Canceled = errors.New("context canceled")var DeadlineExceeded error = deadlineExceededError{}type deadlineExceededError struct{}func (deadlineExceededError) Error() string   { return "context deadline exceeded" }
func (deadlineExceededError) Timeout() bool   { return true }
func (deadlineExceededError) Temporary() bool { return true }

? Canceled:context 被 cancel 時會報此錯誤

? DeadlineExceeded:context 超時時會報此錯誤

三.類的實現

3.1 emptyCtx

通過看它的源碼我們會發現,這個空的上下文其實就是實現了這個context這個接口,對于實現的4個方法都是返回的nil,這就是為什么說他是empty

在之前的版本中他可能是int類型,后來已經被修改了

type emptyCtx struct{}func (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 any) any {return nil
}

? emptyCtx 是一個空的 context

? Deadline 方法會返回一個公元元年時間以及 false 的 flag,標識當前 context 不存在過期時間;

? Done 方法返回一個 nil 值,用戶無論往 nil 中寫入或者讀取數據,均會陷入阻塞;

? Err 方法返回的錯誤永遠為 nil;

? Value 方法返回的 value 同樣永遠為 nil.

3.1.1 context.Background() & context.TODO()

context包主要提供了兩種方式創建context:

  • context.Backgroud()
  • context.TODO()

我們在看代碼的時候,經常可以看到不是Background就是todo作為上下文的起始,那他們有什么區別呢?

兩者的區別

這兩個函數其實只是互為別名,沒有差別,官方給的定義是:

  • context.Background 是上下文的默認值,所有其他的上下文都應該從它衍生(Derived)出來。
  • context.TODO 應該只在不確定應該使用哪種上下文時使用;

所以在大多數情況下,我們都使用context.Background作為起始的上下文向下傳遞。

看一下兩者的底層是什么?

兩者其實都是一個對context的一個繼承

type backgroundCtx struct{ emptyCtx }
type todoCtx struct{ emptyCtx }func Background() Context {return backgroundCtx{}
}func TODO() Context {return todoCtx{}
}

看到這里,你會發現上面的兩種方式是創建根context,不具備任何功能,具體實踐其實還是要依靠context包提供的With系列函數來進行派生:

func WithCancel(parent Context) (ctx Context, cancel CancelFunc)
func WithDeadline(parent Context, deadline time.Time) (Context, CancelFunc)
func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc)
func WithValue(parent Context, key, val interface{}) Context

這四個函數都要基于父Context衍生,通過這些函數,就創建了一顆Context樹,樹的每個節點都可以有任意多個子節點,節點層級可以有任意多個,畫個圖表示一下:

基于一個父Context可以隨意衍生,其實這就是一個Context樹,樹的每個節點都可以有任意多個子節點,節點層級可以有任意多個,每個子節點都依賴于其父節點,例如上圖,我們可以基于Context.Background衍生出四個子contextctx1.0-cancelctx2.0-deadlinectx3.0-timeoutctx4.0-withvalue,這四個子context還可以作為父context繼續向下衍生,即使其中ctx1.0-cancel 節點取消了,也不影響其他三個父節點分支。

創建context方法和context的衍生方法就這些,關于這些with函數,會在后續的使用中看到

3.1.2 With類函數

  1. WithCancel(parent Context)

用途:創建一個新的上下文和取消函數。當調用取消函數時,所有派生自這個上下文的操作將被通知取消。

應用場景:當一個長時間運行的操作需要能夠被取消時。例如,用戶在網頁中點擊“取消”按鈕時,相關的數據庫或 HTTP 請求應立即停止。

2. WithDeadline(parent Context, d time.Time)

用途:創建一個新的上下文,該上下文在指定的時間點自動取消。

應用場景:在請求處理時設置最大執行時間。例如,調用外部 API 時,如果響應時間超過預期,將自動取消請求,以避免無效的等待。

3. WithTimeout(parent Context, timeout time.Duration)

用途:創建一個新的上下文,它會在指定的持續時間內自動取消。

應用場景:適用于設置操作的超時時間,確保系統不會在某個操作上無休止地等待。常用于網絡請求或長時間運行的任務。

4. WithValue(parent Context, key, val interface{})

用途:創建一個新的上下文,并將鍵值對存儲在該上下文中。

應用場景:在處理請求時,將特定的數據(如用戶身份信息、RequestID)在處理鏈中傳遞,而不需要在每個函數參數中顯式傳遞。

3.2 cancelCtx

接下來看一下第二個實現的類吧,看名字就能看出來他是一個帶有取消功能的上下文。

type cancelCtx struct {Contextmu       sync.Mutex            done     atomic.Value          children map[canceler]struct{} // 這里是一個set{}err      error                 cause    error                
}type canceler interface {cancel(removeFromParent bool, err, cause error)Done() <-chan struct{}
}// 這里體現了goland的一個編程哲學
// 作為一個父context,它只需要關注子類的這兩個方法即可,
// 它的子類可能更有能力,但是與父親無關,只需要知道他是否還存在即可
// 會通過就近生成interface的方式,把無關的信息都屏蔽掉。
// 也就是誰使用誰聲明誰管理

? 繼承了一個 context 作為其父 context. 可見,cancelCtx 必然為某個 context 的子 context

? 內置了一把鎖,用以協調并發場景下的資源獲取;

? done:實際類型為 chan struct{},即用以反映 cancelCtx 生命周期的通道;

? children:一個 set,指向 cancelCtx 的所有子 context;

? err:記錄了當前 cancelCtx 的錯誤. 必然為某個 context 的子 context;

? cause:是在go1.20之后加入的字段,主要作用適用于記錄導致context被取消的具體原因

在這里,要加入一些其他的內容----同步調用和異步調用

同步調用,其實就是一種串行的方式,也就是我們平時寫的程序,他是一步一步進行下去,類似一條鏈的形式。

而異步調用則是開辟協程,并且不會阻塞主線程,并且主線程對子協程的感知能力很弱,開辟了多個子協程,就會形成類似樹的形式

就會導致,主線程對子協程的管理能力下降,從而致使協程無法回收,最后導致協程泄露的一個問題。

在創建協程方面,我們要知道一點,如果不知道你創建的協程什么時候結束,你就不應該去創建,不應該濫用并發。

如何解決這個協程的控制呢?

那么今天的主角就是cancelCtx了

先說cancelCtx繼承了Context,對其方法進行了一個重寫,但是并沒有對Deadline方法重寫,而是直接繼承的父類的。Deadline不進行重寫是因為他沒有過期取消的能力。

func (c *cancelCtx) Value(key any) any {// 這里為什么要加入這個判斷,在后續會介紹,主要就是用于判斷// 其自身是否是一個cancelCtx類型,這個cancelCtxKey是一個定值if key == &cancelCtxKey {return c}return value(c.Context, key)
}func (c *cancelCtx) Done() <-chan struct{} {d := c.done.Load()if d != nil {return d.(chan struct{})}c.mu.Lock()defer c.mu.Unlock()d = c.done.Load()if d == nil {d = make(chan struct{})c.done.Store(d)}return d.(chan struct{})
}func (c *cancelCtx) Err() error {c.mu.Lock()err := c.errc.mu.Unlock()return err
}

3.2.1 WithCancel取消控制

既然想實現這種父子聯動的行為,就輪到了With下的一個函數WithCancel函數,通過這個函數從而得到一個cancelCtx對象,從而實現一個對子協程的一個控制

func main()  {ctx,cancel := context.WithCancel(context.Background())// 以context.Background()為父,創建得到一個子context和cancelgo Speak(ctx)time.Sleep(10*time.Second)cancel()time.Sleep(1*time.Second)
}func Speak(ctx context.Context)  {for range time.Tick(time.Second){select {case <- ctx.Done():fmt.Println("我要閉嘴了")returndefault:fmt.Println("balabalabalabala")}}
}

來對這個例子做出一個解釋:

select就相當于是一個多路復用,進行一個監聽的操作,通過這個WithCancel獲取上下文和取消函數

當調用這個cancel函數的時候,就會直接通過這個ctx.Done發送這個取消機制,從而實現一個控制的效果

這里的操作也就是我們常說的超時控制了,當然cancel并沒有涉及到超時,他是通過調用cancel()才可以實現一個關閉。

這個結構體就是cancel取消函數的結構體,他返回的是一個函數類型,所以調用的時候需要加上()

來看下這個函數的具體流程這里先提前告知一點,如果停止一個cancelCtx,則這個它下面所有的子上下文都將被殺死,具體的操作看propagateCancel函數

type CancelFunc func()func WithCancel(parent Context) (ctx Context, cancel CancelFunc) {c := withCancel(parent)//Canceled是一個error,自定義的錯誤信息return c, func() { c.cancel(true, Canceled, nil) }
}func withCancel(parent Context) *cancelCtx {if parent == nil {panic("cannot create context from nil parent")}c := &cancelCtx{}c.propagateCancel(parent, c)return c
}func (c *cancelCtx) propagateCancel(parent Context, child canceler) {c.Context = parent// 這一步就是說父親是emptyCtx,他就沒有必要取消// 所以沒有必要大費周折的取消它,直接就返回就行done := parent.Done()if done == nil {return // parent is never canceled}// 如果父親被取消了,那兒子也應該直接取消,記錄一下取消的錯誤和原因select {case <-done:// parent is already canceledchild.cancel(false, parent.Err(), Cause(parent))returndefault:}// 如果我的父也是cancelCtx,則我需要將孩子加入map里面if p, ok := parentCancelCtx(parent); ok {// parent is a *cancelCtx, or derives from one.p.mu.Lock()if p.err != nil {// parent has already been canceledchild.cancel(false, p.err, p.cause)} else {if p.children == nil {p.children = make(map[canceler]struct{})}p.children[child] = struct{}{}}p.mu.Unlock()return}//檢查父Context是否支持AfterFunc,也就是一個回調機制// if a, ok := parent.(afterFuncer); ok {// parent implements an AfterFunc method.c.mu.Lock()stop := a.AfterFunc(func() {child.cancel(false, parent.Err(), Cause(parent))})c.Context = stopCtx{Context: parent,stop:    stop,}c.mu.Unlock()return}// 開啟一個守護協程,時刻監聽,第一個判斷父親是不是被終止了// 如果父親被終止了,就應該給孩子也砍一刀,讓他們也都終止// 如果孩子被終止了,那就被終止了,什么也不需要處理,傳播具有單向性goroutines.Add(1)go func() {select {case <-parent.Done():child.cancel(false, parent.Err(), Cause(parent))case <-child.Done():}}()
}

parentCancelCtx用于判斷是不是cancelCtx類型

func (c *cancelCtx) Value(key any) any {// 這里為什么要加入這個判斷,在后續會介紹,主要就是用于判斷// 其自身是否是一個cancelCtx類型,這個cancelCtxKey是一個定值if key == &cancelCtxKey {return c}return value(c.Context, key)
}func parentCancelCtx(parent Context) (*cancelCtx, bool) {done := parent.Done()if done == closedchan || done == nil {return nil, false}p, ok := parent.Value(&cancelCtxKey).(*cancelCtx)if !ok {return nil, false}pdone, _ := p.done.Load().(chan struct{})if pdone != done {return nil, false}return p, true
}

再看一下返回的閉包函數吧

cancelCtx.cancel 方法有三個入參,第一個 removeFromParent 是一個 bool 值,表示當前 context 是否需要從父 context 的 children set 中刪除;第二個 err 則是 cancel 后需要展示的錯誤,第三個則表示導致錯誤原因。

func (c *cancelCtx) cancel(removeFromParent bool, err, cause error) {if err == nil {panic("context: internal error: missing cancel error")}if cause == nil {cause = err}c.mu.Lock()if c.err != nil {c.mu.Unlock()return // already canceled}c.err = errc.cause = caused, _ := c.done.Load().(chan struct{})if d == nil {// closedchan 這里是一個全局chan// 關閉,從而取消監聽,Store就是為了確保原子性和可見性c.done.Store(closedchan)} else {close(d)}for child := range c.children {// NOTE: acquiring the child's lock while holding parent's lock.child.cancel(false, err, cause)}c.children = nilc.mu.Unlock()if removeFromParent {removeChild(c.Context, c)}
}// removeChild removes a context from its parent.
func removeChild(parent Context, child canceler) {if s, ok := parent.(stopCtx); ok {s.stop()return}p, ok := parentCancelCtx(parent)if !ok {return}p.mu.Lock()if p.children != nil {delete(p.children, child)}p.mu.Unlock()
}

3.3 timerCtx

接下來看第三個實現的類

type timerCtx struct {cancelCtxtimer *time.Timer // Under cancelCtx.mu.deadline time.Time
}

timerCtx 在 cancelCtx 基礎上又做了一層封裝,除了繼承 cancelCtx 的能力之外,新增了一個 time.Timer 用于定時終止 context;另外新增了一個 deadline 字段用于字段 timerCtx 的過期時間.

這樣就有了實現時停的操作,它對Dealine進行了一個重寫,其他都是繼承的cancelCtx的

Deadline返回的是 deadline time.Time

3.3.1WithTimeout和WithDeadline

func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc) {return WithDeadline(parent, time.Now().Add(timeout))
}func WithDeadline(parent Context, d time.Time) (Context, CancelFunc) {return WithDeadlineCause(parent, d, nil)
}

這兩個函數他們的參數就可以看出區別,context.WithTimeout是指經過的時間段,而WithDeadline則是指時間點。

這里屬于是超時取消。

3.4 valueCtx

type valueCtx struct {Contextkey, val any
}

說一下這個valueCtx吧,在不同位置設置的value其實在查找方面是有問題的,比如你在最下面那一層去存放,也就是B下面的子節點存放value,只有A和B才可以訪問這個value,C和D是無法訪問到的。

3.4.1WithValue

func WithValue(parent Context, key, val any) Context {if parent == nil {panic("cannot create context from nil parent")}if key == nil {panic("nil key")}if !reflectlite.TypeOf(key).Comparable() {panic("key is not comparable")}return &valueCtx{parent, key, val}
}

通過這個函數來設置value值。

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/pingmian/83084.shtml
繁體地址,請注明出處:http://hk.pswp.cn/pingmian/83084.shtml
英文地址,請注明出處:http://en.pswp.cn/pingmian/83084.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

BERT、GPT-3與超越:NLP模型演進全解析

自然語言處理&#xff08;NLP&#xff09;領域近年來經歷了前所未有的變革&#xff0c;從早期的統計方法到如今的深度學習大模型&#xff0c;技術的進步推動了機器理解、生成和交互能力的飛躍。其中&#xff0c;BERT和GPT-3作為兩個里程碑式的模型&#xff0c;分別代表了不同的…

Kanass入門教程- 事項管理

kanass是一款國產開源免費、簡潔易用的項目管理工具&#xff0c;包含項目管理、項目集管理、事項管理、版本管理、迭代管理、計劃管理等相關模塊。工具功能完善&#xff0c;用戶界面友好&#xff0c;操作流暢。本文主要介紹事項管理使用指南。 1、添加事項 事項有多種類型 分…

2025年5月個人工作生活總結

本文為 2025年5月工作生活總結。 研發編碼 一個項目的臨時記錄 月初和另一項目同事向業主匯報方案&#xff0c;兩個項目都不滿意&#xff0c;后來領導做了調整&#xff0c;將項目合并&#xff0c;拆分了好幾大塊。原來我做的一些工作&#xff0c;如數據庫、中間件等&#xff…

? Unity AVProVideo插件自帶播放器 腳本重構 實現視頻激活重置功能

一、功能概述 本筆記記錄直接修改插件自帶的場景播放其中 原始的 MediaPlayerUI 腳本,實現激活時自動重置播放器的功能。 我用的插件版本是 AVPro Video - Ultra Edition 2.7.3 修改后的腳本將具備以下特性: 激活 GameObject 時自動重置播放位置到開頭 可配置是否在重置后自…

5.31 數學復習筆記 22

前面的筆記&#xff0c;全部寫成一段&#xff0c;有點難以閱讀。現在改進一下排版。另外&#xff0c;寫筆記實際上就是圖一個放松呢&#xff0c;關鍵還是在于練習。 目前的計劃是&#xff0c;把講義上面的高數例題搞清楚之后&#xff0c;大量刷練習冊上面的題。感覺不做幾本練…

什么是 WPF 技術?什么是 WPF 樣式?下載、安裝、配置、基本語法簡介教程

什么是 WPF 技術&#xff1f;什么是 WPF 樣式&#xff1f;下載、安裝、配置、基本語法簡介教程 摘要 WPF教程、WPF開發、.NET 8 WPF、Visual Studio 2022 WPF、WPF下載、WPF安裝、WPF配置、WPF樣式、WPF樣式詳解、XAML語法、XAML基礎、MVVM架構、數據綁定、依賴屬性、資源字典…

ROS2與Unitree機器人集成指南

Tested systems and ROS2 distro systemsROS2 distroUbuntu 20.04foxyUbuntu 22.04humblesrc目錄上級才可以colcon build git clone https://github.com/unitreerobotics/unitree_ros2 Install Unitree ROS2 package 1. Dependencies sudo apt install ros-humble-rmw-cyclon…

深入探討集合與數組轉換方法

目錄 1、Arrays.asList() 1.1、方法作用 1.2、內部實現 1.3、修改元素的影響 1.4、注意事項 2、list.toArray() 2.1、方法作用 2.2、內部實現 2.3、修改元素的影響 2.4、特殊情況 1、對象引用 2、數組copy 3、對比總結 4、常見誤區與解決方案 5、實際應用建議…

深入理解交叉熵損失函數——全面推演各種形式

帶你從不一樣的視角綜合認識交叉熵損失&#xff0c;閱讀這篇文章&#xff0c;幫你建立其分類問題&#xff0c;對比學習&#xff0c;行人重識別&#xff0c;人臉識別等問題的聯系&#xff0c;閱讀這篇文章相信對你閱讀各種底層深度學習論文有幫助。 引言 1. 重新理解全連接層&…

STM32之FreeRTOS移植(重點)

RTOS的基本概念 實時操作系統&#xff08;Real Time Operating System&#xff09;的簡稱就叫做RTOS&#xff0c;是指具有實時性、能支持實時控制系統工作的操作系統&#xff0c;RTOS的首要任務就是調度所有可以利用的資源來完成實時控制任務的工作&#xff0c;其次才是提高工…

MySQL connection close 后, mysql server上的行為是什么

本文著重講述的是通過 msql client 連接到 mysql server &#xff0c;發起 update 、 select 操作(由于數據量非常大&#xff0c;所以 update、select 操作都很耗時&#xff0c;即在結果返回前我們有足夠的時間執行一些操作) 。 在客戶端分別嘗試執行 ctrl C 結束關閉 mysql c…

dvwa3——CSRF

LOW&#xff1a; 先嘗試change一組密碼&#xff1a;123456 修改成功&#xff0c;我們觀察上面的url代碼 http://localhost/DVWA/vulnerabilities/csrf/?password_new123456&password_conf123456&ChangeChange# 將password_new部分與password_conf部分改成我們想要的…

Linux 中常見的安全與權限機制

Linux 中常見的安全與權限機制主要包括以下幾類&#xff0c;從文件系統權限到系統級訪問控制&#xff0c;構建了多層次的安全保障體系。 &#x1f510; 一、文件權限與用戶管理 1. 基本權限&#xff08;rwx&#xff09; r&#xff08;read&#xff09;&#xff1a;讀取文件內…

CSS篇-3

1. CSS 中哪些樣式可以繼承&#xff1f;哪些不可以繼承&#xff1f; 可繼承的樣式&#xff1a; 與字體相關的樣式&#xff0c;如&#xff1a;font-size、font-family、color 列表樣式&#xff1a;list-style&#xff08;如 UL、OL 的 list-style-type&#xff09; 不可繼承…

計算機網絡物理層基礎練習

第二章 物理層 填空題 從通信雙方信息交互的方式來看&#xff0c;通信的三種基本方式為單工、半雙工和全雙工。其中&#xff0c;單工數據傳輸只支持數據在一個方向上傳輸&#xff0c;全雙工數據傳輸則允許數據同時在兩個方向上傳輸。最基本的帶通調制方法包括三種&#xff1a…

Redis7底層數據結構解析

redisObject 在 Redis 的源碼中&#xff0c;Redis 會將底層數據結構&#xff08;如 SDS、hash table、skiplist 等&#xff09;統一封裝成一個對象&#xff0c;這個對象叫做 redisObject&#xff0c;也簡稱 robj。 typedef struct redisObject {unsigned type : 4; // 數…

華為OD機試_2025 B卷_靜態掃描(Python,100分)(附詳細解題思路)

題目描述 靜態掃描可以快速識別源代碼的缺陷&#xff0c;靜態掃描的結果以掃描報告作為輸出&#xff1a; 1、文件掃描的成本和文件大小相關&#xff0c;如果文件大小為N&#xff0c;則掃描成本為N個金幣 2、掃描報告的緩存成本和文件大小無關&#xff0c;每緩存一個報告需要…

【Java】在 Spring Boot 中連接 MySQL 數據庫

在 Spring Boot 中連接 MySQL 數據庫是一個常見的任務。Spring Boot 提供了自動配置功能&#xff0c;使得連接 MySQL 數據庫變得非常簡單。以下是詳細的步驟&#xff1a; 一、添加依賴 首先&#xff0c;確保你的pom.xml文件中包含了 Spring Boot 的 Starter Data JPA 和 MySQ…

基于51單片機的音樂盒鍵盤演奏proteus仿真

地址&#xff1a; https://pan.baidu.com/s/1tZCAxQQ7cvyzBfztQpk0UA 提取碼&#xff1a;1234 仿真圖&#xff1a; 芯片/模塊的特點&#xff1a; AT89C52/AT89C51簡介&#xff1a; AT89C51 是一款常用的 8 位單片機&#xff0c;由 Atmel 公司&#xff08;現已被 Microchip 收…

Android Native 之 adbd進程分析

目錄 1、adbd守護進程 2、adbd權限降級 3、adbd命令解析 1&#xff09;adb shell 2&#xff09;adb root 3&#xff09;adb reboot 4、案例 1&#xff09;案例之實現不需要執行adb root命令自動具有root權限 2&#xff09;案例之實現不需要RSA認證直接能夠使用adb she…