【Go 并發控制】上下文 context 源碼

Context

在 Go 服務中,往往由一個獨立的 goroutine 去處理一次請求,但在這個 goroutine 中,可能會開啟別的 goroutine 去執行一些具體的事務,如數據庫,RPC 等,同時,這一組 goroutine 可能還需要共同訪問一些特殊的值,如用戶 token, 請求過期時間等,當一個請求超時后,我們希望與此請求有關的所有 goroutine 都能快速退出,以回收系統資源。

context 包由谷歌開源,在 Go 1.7 時加入標準庫,使用它可以很容易的把特定的值,取消信號, 截止日期傳遞給請求所涉及的所有 goroutine。

context 包的核心是 Context 接口,其結構如下:

type Context interface {Done() <-chan struct{}Err() errorDeadline() (deadline time.Time, ok bool)Value(key interface{}) interface{}
}
  1. Done 返回一個 chan, 表示一個取消信號,當這個通道被關閉時,函數應該立刻結束工作并返回。
  2. Err() 返回一個 error, 表示取消上下文的原因
  3. Deadline 會返回上下文取消的時間
  4. Value 用于從上下文中獲取 key 對應的值

使用

傳遞取消信號(cancelation signals)

正如使用 chan 控制并發一樣,我們希望傳遞給 goroutine 一個信號,一旦接收到這個信號,就立刻停止工作并返回,context 包提供了一個 WithCancel(), 使用它可以很方便的傳遞取消信號。

func useContext(ctx context.Context, id int) {for {select {case <- ctx.Done():fmt.Println("stop", id)returndefault:run(id)}}
}func G2(ctx context.Context) {nCtx, nStop := context.WithCancel(ctx)go G4(nCtx)for {select {case <- ctx.Done():fmt.Println("stop 2")nStop()returndefault:run(2)}}
}func G3(ctx context.Context) {useContext(ctx, 3)
}func G4(ctx context.Context) {useContext(ctx, 4)
}func main() {ctx, done := context.WithCancel(context.Background())go G2(ctx)go G3(ctx)time.Sleep(5*time.Second)done()time.Sleep(5*time.Second)
}

設置截止時間

func G6(ctx context.Context) {for  {select {case <- ctx.Done():t, _ := ctx.Deadline()fmt.Printf("[*] %v done: %v\n", t, ctx.Err())returndefault:fmt.Println("[#] run ...")}}
}func main() {// ctx, done := context.WithTimeout(context.Background(), time.Second * 2)ctx, _ := context.WithTimeout(context.Background(), time.Second * 2)go G6(ctx)//done()time.Sleep(10*time.Second)
}[#] run ...
...
[*] 2020-10-31 20:24:42.0581352 +0800 CST m=+2.008975001 done: context deadline exceeded

傳值

func G7(ctx context.Context) {for  {select {case <- ctx.Done():fmt.Println("cancel", ctx.Value("key"))returndefault:fmt.Println("running ", ctx.Value("key"))time.Sleep(time.Second)}}
}func main() {ctx, _ := context.WithTimeout(context.Background(), time.Second * 2)ctx =  context2.WithValue(ctx, "key", "value")go G7(ctx)time.Sleep(10*time.Second)
}

context 包概覽

context 包的核心是 context.Context 接口,另外有四個 struct 實現了 Context 接口,分別是 emptyCtx, cancelCtx, timerCtx, valueCtx, 其中 emptyCtx 是一個默認的空結構體,其余三個都是在其基礎上添加了各自功能的實現,針對 emptyCtx ,context 包中暴露了兩個方法 Background()TODO() 去創建一個空的 emptyCtx, 而針對后面三種具體的 struct ,context 包總共暴露了四個方法去產生對應的 struct, 他們分別是: WithCancel(), WithDeadLine(), WithTimeout(), WithValue(),對應關系如下:

TODO 和 Background

TODO 和 Background 方法用來返回一個 emptyCtx 類型,他們在實現上都一樣:

var (background = new(emptyCtx)todo       = new(emptyCtx)
)func Background() Context {return background
}func TODO() Context {return todo
}

這兩個方法都會返回一個非空的上下文 emptyCtx,他永遠不會被取消,用于傳遞給其他方法去構建更加復雜的上下文對象,一般默認使用 Background(), 只有在不確定時使用TODO(), 但實際上他們只是名字不同而已。

下面是 emptyCtx 的實現,他確實沒做任何事。

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
}

WithCancel

type cancelCtx struct {Contextmu       sync.Mutex            // 用于同步done     chan struct{}         // 會在 Done 中返回children map[canceler]struct{} // 子上下文列表,done 被關閉后,會遍歷這個 map,關閉所有的子上下文err      error                 // 關閉 chan 產生的異常,在初始化時會被賦值使不為空
}func (c *cancelCtx) Done() <-chan struct{} {c.mu.Lock()if c.done == nil {c.done = make(chan struct{})}d := c.donec.mu.Unlock()return d
}
func WithCancel(parent Context) (ctx Context, cancel CancelFunc) {c := newCancelCtx(parent)propagateCancel(parent, &c)return &c, func() { c.cancel(true, Canceled) }
}

當調用 WithCancel 時, 首先會根據 parent 拷貝一個新的 cancelCtx:

func newCancelCtx(parent Context) cancelCtx {return cancelCtx{Context: parent}
}

然后會調用 propagateCancel 安排子上下文在父上下文結束時結束,最后除了 cancelCtx 的引用外還會返回一個 func, 該方法里調用了 c.cancel(), 也就是當我們調用 done() 時,調用的其實是 c.cancel()

cancel

cancel 的作用是關閉 當前上下文以及子上下文的cancelCtx.done 管道。

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     // 已經關閉,返回}c.err = err    // 通過 err 標識已經關閉if c.done == nil {c.done = closedchan} else {close(c.done)   // 關閉當前 done}// 由于是 map, 所以關閉順序是隨機的for child := range c.children {child.cancel(false, err)   // 遍歷取消所有子上下文}c.children = nil    // 刪除子上下文c.mu.Unlock()if removeFromParent {removeChild(c.Context, c)   // 從父上下文刪除自己}
}

propagateCancel

該函數的作用是保證父上下文結束時子上下文也結束,一方面,在生成子上下文的過程中,如果父親已經被取消,那 child 也會被關閉,另一方面,如果在執行過程中父上下文一直開啟,那就正常把子上下文加入到父上下文的 children 列表中等執行 cancel再關閉。

func propagateCancel(parent Context, child canceler) {done := parent.Done()// 如果父親的 Done 方法返回空,說明父上下文永遠不會被取消// 這種情況對應 ctx, done := context.WithCancel(context.Background())if done == nil {return }// 如果到這父上下文已經被取消了,就關閉當前上下文select {case <-done:child.cancel(false, parent.Err())returndefault:}// 父親沒有被取消if p, ok := parentCancelCtx(parent); ok {p.mu.Lock()// 父親已經取消,關閉自己if p.err != nil {child.cancel(false, p.err)} else {// 把 child 加到 parent 的 children 中if p.children == nil {p.children = make(map[canceler]struct{})}p.children[child] = struct{}{}}p.mu.Unlock()} else {// 父上下文是開發者自定義的類型, 開啟一個 goroutine 監聽父子上下文直到其中一個關閉atomic.AddInt32(&goroutines, +1)go func() {select {case <-parent.Done():child.cancel(false, parent.Err())case <-child.Done():}}()}
}

WithTimeout 和 WithDeadline

type timerCtx struct {cancelCtxtimer *time.Timerdeadline time.Time
}func (c *timerCtx) Deadline() (deadline time.Time, ok bool) {return c.deadline, true
}

timerCtx是在 cancelCtx的基礎上添加了一個定時器和截止時間實現的。

func WithDeadline(parent Context, d time.Time) (Context, CancelFunc) {// 如果傳入的截止時間比父上下文的截止時間晚,也就是說父上下文一定會比子上下文先結束// 這種情況下給子上下文設置截止時間是沒有任何意義的,所以會直接創建一個 cancelCtxif cur, ok := parent.Deadline(); ok && cur.Before(d) {return WithCancel(parent)}// 構建新的 timerCtxc := &timerCtx{cancelCtx: newCancelCtx(parent),deadline:  d,}// 保證子上下文在父上下文關閉時關閉propagateCancel(parent, c)// 計算當前距離截止時間 d 還有多長時間dur := time.Until(d)// 如果已經過了截止時間,關閉子上下文if dur <= 0 {c.cancel(true, DeadlineExceeded) // deadline has already passedreturn c, func() { c.cancel(false, Canceled) }}c.mu.Lock()defer c.mu.Unlock()// c.err == nil 說明當前上下文還沒有被關閉if c.err == nil {// AfterFunc 等待 dur 后會開啟一個 goroutine 執行 傳入的方法,即 c.cancel// 并會返回一個計時器 timer,通過調用 timer 的 Stop 方法可以停止計時取消調用。c.timer = time.AfterFunc(dur, func() {c.cancel(true, DeadlineExceeded)})}return c, func() { c.cancel(true, Canceled) }
}

timerCtxcancel 方法主要還是調用了 cancelCtx.cancel

func (c *timerCtx) cancel(removeFromParent bool, err error) {// 調用 cancelCtx.cancel,關閉子上下文c.cancelCtx.cancel(false, err)// 從父上下文中刪除當前上下文if removeFromParent {removeChild(c.cancelCtx.Context, c)}c.mu.Lock()if c.timer != nil {// 停止計時,取消調用c.timer.Stop()c.timer = nil}c.mu.Unlock()
}

WithTimeout 直接調用了 WithDeadline

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

WithValue

func WithValue(parent Context, key, val interface{}) Context {// key 不能為 nilif key == nil {panic("nil key")}// key 必須是可比較的if !reflectlite.TypeOf(key).Comparable() {panic("key is not comparable")}return &valueCtx{parent, key, val}
}type valueCtx struct {Contextkey, val interface{}
}

The provided key must be comparable and should not be of type string or any other built-in type to avoid collisions between packages using context. Users of WithValue should define their own types for keys.

key 請盡量使用自定義的 struct{}, 避免使用內置數據類型以避免使用 context 包時的沖突

總結

context 包是 Go 1.7 后加入的一種用于復雜場景下并發控制的模型,最核心的接口是 context.Context, 這個結構體中定義了五個待實現的方法,用來實現發送關閉信號,設置 dateline,傳遞值等功能。

context 包的核心思想是以 樹形 組織 goroutine, 創建新上下文時需要給他指定一個父上下文,由此,根上下文對應根 goroutine, 子上下文對應子 Goroutine, 實現靈活的并發控制。

rootContext 一般通過 Background()TODO() 創建,他們會創建一個空的 emptyCtx, 然后如果想要使用 context 包的具體功能,可以使用 WithCancel()WithDateline()WithValue() 將父上下文包裝成具體的上下文對象(cancelCtx, timerCtx, valueCtx),前兩個方法會返回兩個值 (ctx Context, done func()) 調用 done 可以向 goroutine 發送一個關閉信號, goroutine 中監控 ctx.Done() 便可得到這個信號。

cancelCtxtimerCtx 會保持一個 childrentimerCtx 實際上是繼承了 cancelCtx),這是一個 map key 是 canceler , Value 是 struct{} 類型,值并沒什么用,在創建 cancelCtxtimerCtx時,會把當前上下文加入到其父親的 children 中,在父上下文關閉時會遍歷 children 關閉所有的子上下文,并將本上下文從其父上下文的 children 中刪除,由于 map 遍歷的無序性,子上下文關閉的順序也是隨機的。

WithValue() 以及 valueCtx 的實現稍微與前兩個有所不同,一方面 valueCtx 沒有自己實現 Done(), Deadline() 等方法,所以其功能僅限于傳值,另外,在 WithValue() 中并沒有調用 propagateCancel(), 所以 valueCtx 并不會被放在父上下文的 children 中,他自己也沒有 children, 所以使用 valueCtx 作為父上下文是沒有意義的。

如非必要,一般無需使用 WithValue() 的功能傳值,他一般用在傳遞請求對應用戶的認證令牌或用于進行分布式追蹤的請求 ID中。

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

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

相關文章

js設置全局變量ajax中賦值

js設置全局變量&#xff0c;在ajax中給予賦值賦值不上問題解決方案 方案一、 //在全局或某個需要的函數內設置Ajax異步為false&#xff0c;也就是同步. $.ajaxSetup({async : false}); //然后再進行你的Ajax操作 $.post(地址, 參數, function(data, status) { if (status &q…

iOS開發UI篇—模仿ipad版QQ空間登錄界面

一、實現和步驟 1.一般ipad項目在命名的時候可以加一個HD,標明為高清版 2.設置項目的文件結構&#xff0c;分為home和login兩個部分 3.登陸界面的設置 &#xff08;1&#xff09;設置第一個控制器和自定義的控制器類&#xff08;登陸&#xff09;關聯 &#xff08;2&#xff09…

click傳值vue_對vue下點擊事件傳參和不傳參的區別詳解

如下所示&#xff1a;{{btn_text1}}{{btn_text2}}var _vm new Vue({data : {btn_text1 : 點擊1 ,btn_text2 : 點擊2},methods : {test_click1 : function (e) {console.log(test_click1--------------------------) ;console.log(e) ;// 輸出結果&#xff1a;MouseEvent {isTr…

【Golang 源碼】sync.Map 源碼詳解

sync.Map 不安全的 map go 中原生的 map 不是并發安全的&#xff0c;多個 goroutine 并發地去操作一個 map 會拋出一個 panic package main import "fmt" func main() {m : map[string]int {"1": 1, "2": 2,}// 并發寫for i : 0; i < 100;…

oracle中scn(系統改變號)

系統scn&#xff1a; select checkpoint_change# from v$database; 文件scn&#xff1a; select name,checkpoint_change# from v$datafile; 結束scn&#xff1a; select name,last_change# from v$datafile; 數據文件頭部scn…

sicktim571操作手冊_SICK激光傳感器TIM310操作說明書

SICK激光傳感器TIM310操作說明書最近更新時間&#xff1a;2015/1/23 13:31:29提 供 商&#xff1a;資料大小&#xff1a;1.2MB文件類型&#xff1a;PDF 文件下載次數&#xff1a;709次資料類型&#xff1a;瀏覽次數&#xff1a;5192次相關產品&#xff1a;詳細介紹&#xff1a;…

Tengine 安裝配置全過程

在先前的文章中介紹過Tengine&#xff0c;先前只是使用了運維人員配置好的內容&#xff0c;未自己進行過安裝配置。周末閑來無事&#xff0c;對于Tengine進行了嘗試性的安裝。記錄下面方便以后再做改進。Tengine官網上有個非常簡單的教程&#xff0c;中間并未涉及到一些常用的設…

【Go】sync.WaitGroup 源碼分析

WaitGroup sync.WaitGroup 用于等待一組 goroutine 返回&#xff0c;如&#xff1a; var wg sync.WaitGroup{}func do() {time.Sleep(time.Second)fmt.Println("done")wg.Done() }func main() {go do()go do()wg.Add(2)wg.Wait()fmt.Println("main done"…

什么是響應式設計?為什么要做響應式設計?響應式設計的基本原理是什么?...

頁面的設計和開發應當根據用戶行為以及設備環境&#xff08;系統平臺、屏幕尺寸、屏幕定向等&#xff09;進行相應的響應和調整。具體的實踐方式由多方面組成&#xff0c;包括彈性網格和布局、圖片、css media query的使用等。無論用戶正在使用筆記本還是iPad&#xff0c;我們的…

三個數相減的平方公式_快收好這份小學數學公式大全!孩子遇到數學難題時肯定用得上...

必背定義、定理公式1.三角形的面積&#xff1d;底高2 公式 S&#xff1d; ah22.正方形的面積&#xff1d;邊長邊長公式 S&#xff1d; aa3.長方形的面積&#xff1d;長寬公式 S&#xff1d; ab4.平行四邊形的面積&#xff1d;底高公式 S&#xff1d; ah5.梯形的面積&#xff1d…

Eclipse 控制console

http://blog.csdn.net/leidengyan/article/details/5686691

【Go】sync.RWMutex源碼分析

RWMutex 讀寫鎖相較于互斥鎖有更低的粒度&#xff0c;它允許并發讀&#xff0c;因此在讀操作明顯多于寫操作的場景下能減少鎖競爭的次數&#xff0c;提高程序效率。 type RWMutex struct {w Mutex // held if there are pending writerswriterSem uint32 // sem…

add.attribute向前端傳_前端知識-概念篇

1、一次完整的HTTP事務是怎樣的一個過程&#xff1f;基本流程&#xff1a;a. 域名解析b. 發起TCP的3次握手c. 建立TCP連接后發起http請求d. 服務器端響應http請求&#xff0c;瀏覽器得到html代碼e. 瀏覽器解析html代碼&#xff0c;并請求html代碼中的資源f. 瀏覽器對頁面進行渲…

【數據庫】一篇文章搞懂數據庫隔離級別那些事(LBCC,MVCC)

MySQL 事務 文章比較長&#xff0c;建議分段閱讀 后續如果有改動會在 Junebao.top 之前對事務的了解僅限于知道要么全部執行&#xff0c;要么全部不執行&#xff0c;能背出 ACID 和隔離級別&#xff0c;知其然但不知其所以然&#xff0c;現在覺得非常有必要系統學一下&#xff…

AFNetworking網絡請求與圖片上傳工具(POST)

AFNetworking網絡請求與圖片上傳工具&#xff08;POST&#xff09; .h文件 #import <Foundation/Foundation.h>/** 成功Block */ typedef void(^SuccessBlockType) (id responsData); /** 失敗Block */ typedef void(^FaileBlockType) (NSError *error);interface NetD…

api商品分享源碼_SSM框架高并發和商品秒殺項目高并發秒殺API源碼免費分享

前言&#xff1a;一個整合SSM框架的高并發和商品秒殺項目,學習目前較流行的Java框架組合實現高并發秒殺API源碼獲取&#xff1a;關注頭條號轉發文章之后私信【秒殺】查看源碼獲取方式&#xff01;項目的來源項目的來源于國內IT公開課平臺,質量沒的說,很適合學習一些技術的基礎,…

Golang 定時任務 github/robfig/cron/v3 使用與源碼解析

Cron 源碼閱讀 robfig/cron/v3 是一個 Golang 的定時任務庫&#xff0c;支持 cron 表達式。Cron 的源碼真實教科書級別的存在&#xff08;可能是我菜 …&#xff09;,真的把低耦合高內聚體現地淋漓盡致&#xff0c;另外其中涉及的裝飾器模式&#xff0c;并發處理等都很值得學習…

修改 cmd 字體為 Consolas

windows 下的 cmd 窗口默認的字體有點難看&#xff0c;長時間使用操作 node.js 有點小疲勞&#xff0c;可以修改注冊表替換字體為 Consolas&#xff0c;并且可以全屏 cmd 窗口&#xff0c;代碼如下&#xff1a; Windows Registry Editor Version 5.00 [HKEY_CURRENT_USER\Conso…

mac下安裝前端模板引擎Jinja2

在mac本上安裝Jinja2&#xff0c;搜索網上介紹的經驗&#xff0c;都是說使用easy_install或者pip安裝&#xff0c;比如 #sudo easy_install Jinja2 #sudo pip install Jinja2 也有直接使用 #easy_install Jinja2的&#xff0c;但是我使用上述命令安裝總是不成功&#xff0c;提示…

為什么要用python不用origin_Python告訴你為什么百度已死

Python3爬蟲百度一下&#xff0c;坑死你&#xff1f;一、寫在前面這個標題是借用的路人甲大佬的一篇文章的標題(百度一下&#xff0c;坑死你)&#xff0c;而且這次的爬蟲也是看了這篇文章后才寫出來的&#xff0c;感興趣的可以先看下這篇文章。前段時間有篇文章《搜索引擎百度已…