<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          【Go進(jìn)階—并發(fā)編程】Context

          共 10902字,需瀏覽 22分鐘

           ·

          2022-03-08 08:52

          作者:與昊

          來源:SegmentFault  思否社區(qū) 


          Context 是 Go 應(yīng)用開發(fā)常用的并發(fā)控制技術(shù),它與 WaitGroup 最大的不同點(diǎn)是 Context 對(duì)于派生 goroutine 有更強(qiáng)的控制力,它可以控制多級(jí)的 goroutine。


          盡管有很多的爭(zhēng)議,但是在很多場(chǎng)景下使用 Context 都很方便,所以現(xiàn)在它已經(jīng)在 Go 生態(tài)圈中傳播開來了,包括很多的 Web 應(yīng)用框架,都切換成了標(biāo)準(zhǔn)庫的 Context。標(biāo)準(zhǔn)庫中的 database/sql、os/exec、net、net/http 等包中都使用到了 Context。而且,如果遇到了下面的一些場(chǎng)景,也可以考慮使用 Context:


          • 上下文信息傳遞 ,比如處理 http 請(qǐng)求、在請(qǐng)求處理鏈路上傳遞信息;

          • 控制子 goroutine 的運(yùn)行;

          • 超時(shí)控制的方法調(diào)用;

          • 可以取消的方法調(diào)用。


          實(shí)現(xiàn)原理



          接口定義


          包 context 定義了 Context 接口,Context 的具體實(shí)現(xiàn)包括 4 個(gè)方法,分別是 Deadline、Done、Err 和 Value,如下所示:

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

          Deadline 方法會(huì)返回這個(gè) Context 被取消的截止日期。如果沒有設(shè)置截止日期,ok 的值是 false。后續(xù)每次調(diào)用這個(gè)對(duì)象的 Deadline 方法時(shí),都會(huì)返回和第一次調(diào)用相同的結(jié)果。

          Done 方法返回一個(gè) Channel 對(duì)象,基本上都會(huì)在 select 語句中使用。在 Context 被取消時(shí),此 Channel 會(huì)被 close,如果沒被取消,可能會(huì)返回 nil。當(dāng) Done 被 close 的時(shí)候,可以通過 ctx.Err 獲取錯(cuò)誤信息。

          關(guān)于 Err 方法,你必須要記住的知識(shí)點(diǎn)就是:如果 Done 沒有被 close,Err 方法返回 nil;如果 Done 被 close,Err 方法會(huì)返回 Done 被 close 的原因。

          Value 返回此 ctx 中和指定的 key 相關(guān)聯(lián)的 value。

          Context 中實(shí)現(xiàn)了 2 個(gè)常用的生成頂層 Context 的方法:

          • context.Background():返回一個(gè)非 nil 的、空的 Context,沒有任何值,不會(huì)被 cancel,不會(huì)超時(shí),沒有截止日期。一般用在主函數(shù)、初始化、測(cè)試以及創(chuàng)建根 Context 的時(shí)候。

          • context.TODO():返回一個(gè)非 nil 的、空的 Context,沒有任何值,不會(huì)被 cancel,不會(huì)超時(shí),沒有截止日期。當(dāng)你不清楚是否該用 Context,或者目前還不知道要傳遞一些什么上下文信息的時(shí)候,就可以使用這個(gè)方法。


          事實(shí)上,它們兩個(gè)底層的實(shí)現(xiàn)是一模一樣的。絕大多數(shù)情況下可以直接使用 context.Background。

          在使用 Context 的時(shí)候,有一些約定俗成的規(guī)則:

          1. 一般函數(shù)使用 Context 的時(shí)候,會(huì)把這個(gè)參數(shù)放在第一個(gè)參數(shù)的位置。

          2. 從來不把 nil 當(dāng)做 Context 類型的參數(shù)值,可以使用 context.Background() 創(chuàng)建一個(gè)空的上下文對(duì)象,也不要使用 nil。

          3. Context 只用來臨時(shí)做函數(shù)之間的上下文透?jìng)?,不能持久?Context 或者把 Context 長(zhǎng)久保存。把 Context 持久化到數(shù)據(jù)庫、本地文件或者全局變量、緩存中都是錯(cuò)誤的用法。

          4. key 的類型不推薦字符串類型或者其它內(nèi)建類型,否則容易在包之間使用 Context 時(shí)候產(chǎn)生沖突。使用 WithValue 時(shí),key 的類型應(yīng)該是自己定義的類型。

          5. 常常使用 struct{} 作為底層類型定義 key 的類型。對(duì)于 exported key 的靜態(tài)類型,常常是接口或者指針。這樣可以盡量減少內(nèi)存分配。


          context 包中實(shí)現(xiàn) Context 接口的 struct,除了用于 context.Background() 的 emptyCtx 外,還有 cancelCtx、timerCtx 和 valueCtx 三種。

          cancelCtx

          type cancelCtx struct {
              Context

              mu       sync.Mutex            // 互斥鎖
              done     atomic.Value          // 調(diào)用 cancel 時(shí)會(huì)關(guān)閉的 channel
              children map[canceler]struct{} // 記錄了由此 Context 派生的所有 child,此 Context 被 cancel 時(shí)會(huì)同時(shí) cancel 所有 child
              err      error                 // 錯(cuò)誤信息
          }

          WithCancel

          cancelCtx 是通過 WithCancel 方法生成的。我們常常在一些需要主動(dòng)取消長(zhǎng)時(shí)間的任務(wù)時(shí),創(chuàng)建這種類型的 Context,然后把這個(gè) Context 傳給長(zhǎng)時(shí)間執(zhí)行任務(wù)的 goroutine。當(dāng)需要中止任務(wù)時(shí),我們就可以 cancel 這個(gè) Context,這樣長(zhǎng)時(shí)間執(zhí)行任務(wù)的 goroutine,就可以通過檢查這個(gè) Context,知道 Context 已經(jīng)被取消了。

          WithCancel 返回值中的第二個(gè)值是一個(gè) cancel 函數(shù)。記住,不是只有你想中途放棄,才去調(diào)用 cancel,只要你的任務(wù)正常完成了,就需要調(diào)用 cancel,這樣,這個(gè) Context 才能釋放它的資源(通知它的 children 處理 cancel,從它的 parent 中把自己移除,甚至釋放相關(guān)的 goroutine)。

          看下核心源碼:

          func WithCancel(parent Context) (ctx Context, cancel CancelFunc) {
              if parent == nil {
                  panic("cannot create context from nil parent")
              }
              c := newCancelCtx(parent)
              propagateCancel(parent, &c)
              return &c, func() { c.cancel(true, Canceled) }
          }

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

          func propagateCancel(parent Context, child canceler) {
              done := parent.Done()

              if p, ok := parentCancelCtx(parent); ok {
                  p.mu.Lock()
                  if p.err != nil {
                      // parent 已經(jīng)取消了,直接取消子 Context
                      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 {
                  atomic.AddInt32(&goroutines, +1)
                  // 沒有 parent 可以“掛載”,啟動(dòng)一個(gè) goroutine 監(jiān)聽 parent 的 cancel,同時(shí) cancel 自身
                  go func() {
                      select {
                      case <-parent.Done():
                          child.cancel(false, parent.Err())
                      case <-child.Done():
                      }
                  }()
              }
          }

          代碼中調(diào)用的 propagateCancel 方法會(huì)順著 parent 路徑往上找,直到找到一個(gè) cancelCtx,或者為 nil。如果不為空,就把自己加入到這個(gè) cancelCtx 的 child,以便這個(gè) cancelCtx 被取消的時(shí)候通知自己。如果為空,會(huì)新起一個(gè) goroutine,由它來監(jiān)聽 parent 的 Done 是否已關(guān)閉。

          當(dāng)這個(gè) cancelCtx 的 cancel 函數(shù)被調(diào)用的時(shí)候,或者 parent 的 Done 被 close 的時(shí)候,這個(gè) cancelCtx 的 Done 才會(huì)被 close。

          cancel 是向下傳遞的,如果一個(gè) WithCancel 生成的 Context 被 cancel 時(shí),如果它的子 Context(也有可能是孫,或者更低,依賴子的類型)也是 cancelCtx 類型的,就會(huì)被 cancel。

          cancel

          cancel 方法的作用是 close 自己及其后代的 done 通道,達(dá)到通知取消的目的。WithCancel 方法的第二個(gè)返回值 cancel 就是本函數(shù)。來看一下主要代碼實(shí)現(xiàn):

          func (c *cancelCtx) cancel(removeFromParent bool, err error) {
              c.mu.Lock()
              // 設(shè)置 cancel 的原因
              c.err = err 
              // 關(guān)閉自身的 done 通道
              d, _ := c.done.Load().(chan struct{})
              if d == nil {
                  c.done.Store(closedchan)
              } else {
                  close(d)
              }
              // 遍歷所有 children,逐個(gè)調(diào)用 cancel 方法
              for child := range c.children {
                  child.cancel(false, err)
              }
              c.children = nil
              c.mu.Unlock()

              // 正常情況下,需要將自身從 parent 的 children 切片中刪除
              if removeFromParent {
                  removeChild(c.Context, c)
              }
          }

          timerCtx


          type timerCtx struct {
              cancelCtx
              timer *time.Timer 

              deadline time.Time
          }

          timerCtx 在 cancelCtx 基礎(chǔ)上增加了 deadline 用于標(biāo)示自動(dòng) cancel 的最終時(shí)間,而 timer 就是一個(gè)觸發(fā)自動(dòng) cancel 的定時(shí)器。timerCtx 可以由 WithDeadline 和 WithTimeout 生成, WithTimeout 實(shí)際調(diào)用了 WithDeadline,二者實(shí)現(xiàn)原理一致,只不過使用語境不一樣:WithDeadline 是指定最后期限,WithTimeout 是指定最長(zhǎng)存活時(shí)間。

          WithDeadline

          來看一下 WithDeadline 方法的實(shí)現(xiàn):

          func WithDeadline(parent Context, d time.Time) (Context, CancelFunc) {
              // 如果parent的截止時(shí)間更早,直接返回一個(gè)cancelCtx即可
              if cur, ok := parent.Deadline(); ok && cur.Before(d) {
                  return WithCancel(parent)
              }
              c := &timerCtx{
                  cancelCtx: newCancelCtx(parent),
                  deadline:  d,
              }
              propagateCancel(parent, c) // 同cancelCtx的處理邏輯
              dur := time.Until(d)
              if dur <= 0 { //當(dāng)前時(shí)間已經(jīng)超過了截止時(shí)間,直接cancel
                  c.cancel(true, DeadlineExceeded)
                  return c, func() { c.cancel(false, Canceled) }
              }
              c.mu.Lock()
              defer c.mu.Unlock()
              if c.err == nil {
                  // 設(shè)置一個(gè)定時(shí)器,到截止時(shí)間后取消
                  c.timer = time.AfterFunc(dur, func() {
                      c.cancel(true, DeadlineExceeded)
                  })
              }
              return c, func() { c.cancel(true, Canceled) }
          }

          WithDeadline 會(huì)返回一個(gè) parent 的副本,并且設(shè)置了一個(gè)不晚于參數(shù) d 的截止時(shí)間,類型為 timerCtx(或者是 cancelCtx)。

          如果它的截止時(shí)間晚于 parent 的截止時(shí)間,那么就以 parent 的截止時(shí)間為準(zhǔn),并返回一個(gè)類型為 cancelCtx 的 Context,因?yàn)?parent 的截止時(shí)間到了,就會(huì)取消這個(gè) cancelCtx。如果當(dāng)前時(shí)間已經(jīng)超過了截止時(shí)間,就直接返回一個(gè)已經(jīng)被 cancel 的 timerCtx。否則就會(huì)啟動(dòng)一個(gè)定時(shí)器,到截止時(shí)間取消這個(gè) timerCtx。

          綜合起來,timerCtx 的 Done 被 Close 掉,主要是由下面的某個(gè)事件觸發(fā)的:

          • 截止時(shí)間到了;

          • cancel 函數(shù)被調(diào)用;

          • parent 的 Done 被 close。


          和 cancelCtx 一樣,WithDeadline(WithTimeout)返回的 cancel 一定要調(diào)用,并且要盡可能早地被調(diào)用,這樣才能盡早釋放資源,不要單純地依賴截止時(shí)間被動(dòng)取消。

          valueCtx


          type valueCtx struct {
              Context
              key, val interface{}
          }

          valueCtx 只是在 Context 基礎(chǔ)上增加了一個(gè) key-value 對(duì),用于在各級(jí)協(xié)程間傳遞一些數(shù)據(jù)。

          WithValue 基于 parent Context 生成一個(gè)新的 valueCtx,保存了一個(gè) key-value 鍵值對(duì)。valueCtx 覆蓋了 Value 方法,優(yōu)先從自己的存儲(chǔ)中檢查這個(gè) key,不存在的話會(huì)從 parent 中繼續(xù)檢查。



          點(diǎn)擊左下角閱讀原文,到 SegmentFault 思否社區(qū) 和文章作者展開更多互動(dòng)和交流,掃描下方”二維碼“或在“公眾號(hào)后臺(tái)回復(fù)“ 入群 ”即可加入我們的技術(shù)交流群,收獲更多的技術(shù)文章~

          - END -


          瀏覽 58
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          評(píng)論
          圖片
          表情
          推薦
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  操女人插女人网站 | 婷婷亚洲天堂 | 亚洲精品国产精品国自产观看 | 啪啪免费视频 | 亚洲性爱a |