一文帶你更方便的控制 goroutine

dustinny發表於2021-04-19

  從整體分析來看,併發元件主要透過channel+mutex控制程式中協程之間溝通。


  Do not communicate by sharing memory;instead,share memory by communicating.


  不要透過共享記憶體來通訊,而應透過通訊來共享記憶體。


  本篇來聊go-zero對Go中goroutine支援的併發元件。


  我們回顧一下,go原生支援的goroutine控制的工具有哪些?


  go func()開啟一個協程


  sync.WaitGroup控制多個協程任務編排


  sync.Cond協程喚醒或者是協程等待


  那可能會問go-zero為什麼還要拿出來講這些?回到go-zero的設計理念:工具大於約定和文件。


  那麼就來看看,go-zero提供哪些工具?


  threading


  雖然go func()已經很方便,但是有幾個問題:


  如果協程異常退出,無法追蹤異常棧


  某個異常請求觸發panic,應該做故障隔離,而不是整個程式退出,容易被攻擊


  我們看看core/threading包提供了哪些額外選擇:


  func GoSafe(fn func()){


  go RunSafe(fn)


  }


  func RunSafe(fn func()){


  defer rescue.Recover()


  fn()


  }


  func Recover(cleanups...func()){


  for _,cleanup:=range cleanups{


  cleanup()


  }


  if p:=recover();p!=nil{


  logx.ErrorStack(p)


  }


  }


  GoSafe


  threading.GoSafe()就幫你解決了這個問題。開發者可以將自己在協程中需要完成邏輯,以閉包的方式傳入,由GoSafe()內部go func();


  當開發者的函式出現異常退出時,會在Recover()中列印異常棧,以便讓開發者更快確定異常發生點和呼叫棧。


  NewWorkerGroup


  我們再看第二個:WaitGroup。日常開發,其實WaitGroup沒什麼好說的,你需要N個協程協作:wg.Add(N),等待全部協程完成任務:wg.Wait(),同時完成一個任務需要手動wg.Done()。


  可以看的出來,在任務開始->結束->等待,整個過程需要開發者關注任務的狀態然後手動修改狀態。


  NewWorkerGroup就幫開發者減輕了負擔,開發者只需要關注:


  任務邏輯【函式】


  任務數【workers】


  然後啟動WorkerGroup.Start(),對應任務數就會啟動:


  func(wg WorkerGroup)Start(){


  //包裝了sync.WaitGroup


  group:=NewRoutineGroup()


  for i:=0;i<wg.workers;i++{


  //內部維護了wg.Add(1)wg.Done()


  //同時也是goroutine安全模式下進行的


  group.RunSafe(wg.job)


  }


  group.Wait()


  }


  worker的狀態會自動管理,可以用來固定數量的worker來處理訊息佇列的任務,用法如下:


  func main(){


  group:=NewWorkerGroup(func(){


  //process tasks


  },runtime.NumCPU())


  group.Start()


  }


  Pool


  這裡的Pool不是sync.Pool。sync.Pool有個不方便的地方是它池化的物件可能會被垃圾回收掉,這個就讓開發者疑惑了,不知道自己建立並存入的物件什麼時候就沒了。


  go-zero中的pool:


  pool中的物件會根據使用時間做懶銷燬;


  使用cond做物件消費和生產的通知以及阻塞;


  開發者可以自定義自己的生產函式,銷燬函式;


  那我來看看生產物件,和消費物件在pool中時怎麼實現的:


  func(p*Pool)Get()interface{}{


  //呼叫cond.Wait時必須要持有c.L的鎖


  p.lock.Lock()


  defer p.lock.Unlock()


  for{


  //1.pool中物件池是一個用連結串列連線的nodelist


  if p.head!=nil{


  head:=p.head


  p.head=head.next


  //1.1如果當前節點:當前時間>=上次使用時間+物件最大存活時間


  if p.maxAge>0&&head.lastUsed+p.maxAge<timex.Now(){


  p.created--


  //說明當前節點已經過期了->銷燬節點對應的物件,然後繼續尋找下一個節點


  //【⚠️:不是銷燬節點,而是銷燬節點對應的物件】


  p.destroy(head.item)


  continue


  }else{


  return head.item


  }


  }


  //2.物件池是懶載入的,get的時候才去建立物件連結串列


  if p.created<p.limit{


  p.created++


  //由開發者自己傳入:生產函式


  return p.create()


  }


  p.cond.Wait()


  }


  }


  func(p*Pool)Put(x interface{}){


  if x==nil{


  return


  }


  //互斥訪問pool中nodelist


  p.lock.Lock()


  defer p.lock.Unlock()


  p.head=&node{


  item:x,


  next:p.head,


  lastUsed:timex.Now(),


  }


  //放入head,通知其他正在get的協程【極為關鍵】


  p.cond.Signal()


  }


  上述就是go-zero對Cond的使用。可以類比生產者-消費者模型,只是在這裡沒有使用channel做通訊,而是用Cond。這裡有幾個特性:


  Cond和一個Locker關聯,可以利用這個Locker對相關的依賴條件更改提供保護。


  Cond可以同時支援Signal和Broadcast方法,而Channel只能同時支援其中一種。


  總結


  工具大於約定和文件,一直是go-zero設計主旨之一;也同時將平時業務沉澱到元件中,這才是框架和元件的意義。


來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/69995861/viewspace-2768661/,如需轉載,請註明出處,否則將追究法律責任。

相關文章