更簡的併發程式碼,更強的併發控制

kevwan發表於2021-04-12

有沒感覺 Gosync 包不夠用?有沒遇到型別沒有 sync/atomic 支援?

我們一起看看 go-zerosyncx 包對標準庫的一些增值補充。

github.com/tal-tech/go-zero/tree/m...

name 作用
AtomicBool bool型別 原子類
AtomicDuration Duration有關 原子類
AtomicFloat64 float64型別 原子類
Barrier 欄柵【將加鎖解鎖包裝】
Cond 條件變數
DoneChan 優雅通知關閉
ImmutableResource 建立後不會修改的資源
Limit 控制請求數
LockedCalls 確保方法的序列呼叫
ManagedResource 資源管理
Once 提供 once func
OnceGuard 一次性使用的資源管理
Pool pool,簡單的池
RefResource 引用計數的資源
ResourceManager 資源管理器
SharedCalls 類似 singflight 的功能
SpinLock 自旋鎖:自旋+CAS
TimeoutLimit Limit + timeout 控制

下面開始對以上庫元件做分別介紹。

atomic

因為沒有 泛型 支援,所以才會出現多種型別的原子類支援。以下采用 float64 作為例子:

func (f *AtomicFloat64) Add(val float64) float64 {
    for {
        old := f.Load()
        nv := old + val
        if f.CompareAndSwap(old, nv) {
            return nv
        }
    }
}

func (f *AtomicFloat64) CompareAndSwap(old, val float64) bool {
    return atomic.CompareAndSwapUint64((*uint64)(f), math.Float64bits(old), math.Float64bits(val))
}

func (f *AtomicFloat64) Load() float64 {
    return math.Float64frombits(atomic.LoadUint64((*uint64)(f)))
}

func (f *AtomicFloat64) Set(val float64) {
    atomic.StoreUint64((*uint64)(f), math.Float64bits(val))
}
  • Add(val):如果 CAS 失敗,不斷for迴圈重試,獲取 old val,並set old+val;

  • CompareAndSwap(old, new):呼叫底層 atomicCAS

  • Load():呼叫 atomic.LoadUint64 ,然後轉換

  • Set(val):呼叫 atomic.StoreUint64

至於其他型別,開發者想自己擴充套件自己想要的型別,可以依照上述,基本上呼叫原始 atomic 操作,然後轉換為需要的型別,比如:遇到 bool 可以藉助 0, 1 來分辨對應的 false, true

Barrier

這裡 Barrier 只是將業務函式操作封裝,作為閉包傳入,內部將 lock 操作的加鎖解鎖自行解決了【防止開發者加鎖了忘記解鎖】

func (b *Barrier) Guard(fn func()) {
    b.lock.Lock()
    defer b.lock.Unlock()
  // 自己的業務邏輯
    fn()
}

Cond/Limit/TimeoutLimit

這個資料結構和 Limit 一起組成了 TimeoutLimit ,這裡將這3個一起講:

func NewTimeoutLimit(n int) TimeoutLimit {
    return TimeoutLimit{
        limit: NewLimit(n),
        cond:  NewCond(),
    }
}

func NewLimit(n int) Limit {
    return Limit{
        pool: make(chan lang.PlaceholderType, n),
    }
}
  • limit 這裡是有緩衝的 channel
  • cond 是無緩衝的;

所以這裡結合名字來理解:因為 Limit 是限制某一種資源的使用,所以需要預先在資源池中放入預置數量的資源;Cond 類似閥門,需要兩邊都準備好,才能進行資料交換,所以使用無緩衝,同步控制。

這裡我們看看 stores/mongo 中關於 session 的管理,來理解 資源控制:

func (cs *concurrentSession) takeSession(opts ...Option) (*mgo.Session, error) {
  // 選項引數注入
    ...
  // 看 limit 中是否還能取出資源
    if err := cs.limit.Borrow(o.timeout); err != nil {
        return nil, err
    } else {
        return cs.Copy(), nil
    }
}

func (l TimeoutLimit) Borrow(timeout time.Duration) error {
  // 1. 如果還有 limit 中還有資源,取出一個,返回
    if l.TryBorrow() {
        return nil
    }
    // 2. 如果 limit 中資源已經用完了
    var ok bool
    for {
    // 只有 cond 可以取出一個【無快取,也只有 cond <- 此條才能通過】
        timeout, ok = l.cond.WaitWithTimeout(timeout)
    // 嘗試取出一個【上面 cond 通過時,就有一個資源返回了】
    // 看 `Return()`
        if ok && l.TryBorrow() {
            return nil
        }
        // 超時控制
        if timeout <= 0 {
            return ErrTimeout
        }
    }
}

func (l TimeoutLimit) Return() error {
  // 返回去一個資源
    if err := l.limit.Return(); err != nil {
        return err
    }
    // 同步通知另一個需要資源的協程【實現了閥門,兩方交換】
    l.cond.Signal()
    return nil
}

資源管理

同資料夾中還有 ResourceManager,從名字上類似,這裡將兩個元件放在一起講解。

先從結構上:

type ManagedResource struct {
  // 資源
    resource interface{}
    lock     sync.RWMutex
  // 生成資源的邏輯,由開發者自己控制
    generate func() interface{}
  // 對比資源
    equals   func(a, b interface{}) bool
}

type ResourceManager struct {
  // 資源:這裡看得出來是 I/O,
    resources   map[string]io.Closer
    sharedCalls SharedCalls
  // 對資源map互斥訪問
    lock        sync.RWMutex
}

然後來看獲取資源的方法簽名:

func (manager *ResourceManager) GetResource(key, create func() (io.Closer, error)) (io.Closer, error)

// 獲取一個資源(有就直接獲取,沒有生成一個)
func (mr *ManagedResource) Take() interface{}
// 判斷這個資源是否不符合傳入的判斷要求,不符合則重置
func (mr *ManagedResource) MarkBroken(resource interface{})
  1. ResourceManager 使用 SharedCalls 做防重複請求,並將資源快取在內部的 sourMap;另外傳入的 create funcIO 操作有關,常見用在網路資源的快取;

  2. ManagedResource 快取資源沒有 map 而是單一的 interface ,說明只有一份,但是它提供了 Take() 和傳入 generate()說明可以讓開發者自行更新 resource

所以在用途上:

  • ResourceManager:用在網路資源的管理。如:資料庫連線管理;
  • ManagedResource:用在一些變化資源,可以做資源前後對比,達到更新資源。如:token 管理和驗證

RefResource

這個就和 GC 中引用計數類似:

  • Use() -> ref++
  • Clean() -> ref--; if ref == 0 -> ref clean
func (r *RefResource) Use() error {
  // 互斥訪問
    r.lock.Lock()
    defer r.lock.Unlock()
    // 清除標記
    if r.cleaned {
        return ErrUseOfCleaned
    }
    // 引用 +1
    r.ref++
    return nil
}

SharedCalls

一句話形容:使用SharedCalls可以使得同時多個請求只需要發起一次拿結果的呼叫,其他請求”坐享其成”,這種設計有效減少了資源服務的併發壓力,可以有效防止快取擊穿

這個元件被反覆應用在其他元件中,上面說的 ResourceManager

類似當需要高頻併發訪問一個資源時,就可以使用 SharedCalls 快取。

// 當多個請求同時使用Do方法請求資源時
func (g *sharedGroup) Do(key string, fn func() (interface{}, error)) (interface{}, error) {
  // 先申請加鎖
  g.lock.Lock()

  // 根據key,獲取對應的call結果,並用變數c儲存
  if c, ok := g.calls[key]; ok {
    // 拿到call以後,釋放鎖,此處call可能還沒有實際資料,只是一個空的記憶體佔位
    g.lock.Unlock()
    // 呼叫wg.Wait,判斷是否有其他goroutine正在申請資源,如果阻塞,說明有其他goroutine正在獲取資源
    c.wg.Wait()
    // 當wg.Wait不再阻塞,表示資源獲取已經結束,可以直接返回結果
    return c.val, c.err
  }

  // 沒有拿到結果,則呼叫makeCall方法去獲取資源,注意此處仍然是鎖住的,可以保證只有一個goroutine可以呼叫makecall
  c := g.makeCall(key, fn)
  // 返回撥用結果
  return c.val, c.err
}

總結

不重複造輪子,一直是 go-zero 設計主旨之一;也同時將平時業務沉澱到元件中,這才是框架和元件的意義。

關於 go-zero 更多的設計和實現文章,可以持續關注我們。歡迎大家去關注和使用。

專案地址

github.com/tal-tech/go-zero

歡迎使用 go-zero 並 star 支援我們!

微信交流群

關注『微服務實踐』公眾號並回復 進群 獲取社群群二維碼。

本作品採用《CC 協議》,轉載必須註明作者和本文連結

相關文章