博客 / 詳情

返回

腦抽研究生Go併發-5-總結-核心 sync 包、Channel 與 Context、高級併發模式與擴展庫、分佈式併發

總結

第一部分:核心 sync 包 —— 本地併發

sync 包裏的這些工具,是處理單進程內、多個 goroutine 之間協同問題的基礎

1. 鎖 (Mutex & RWMutex)

  • 核心作用保護共享資源,保證數據一致性。 任何時候,只允許一個(或一類)goroutine 進入“臨界區”。
  • 精華

    • Mutex (互斥鎖):最簡單粗暴的鎖,一次只能進一個,不管他是“讀”還是“寫”。

      • 決策口訣“情況不明,先上 Mutex。” 它是最安全、最通用的選擇。
    • RWMutex (讀寫鎖):更智能的鎖。

      • 讀鎖 (RLock):允許多個“讀者”同時進入不同的隔間,互不干擾。
      • 寫鎖 (Lock):當有人要“寫操作“時,會將讀者清場,誰也不能進。
      • 決策口訣“讀多寫少,性能瓶頸,才用 RWMutex。” 不要濫用它,因為它的內部實現比 Mutex 複雜,在讀寫均衡的場景下,性能可能還不如 Mutex。
    • 共同的“坑”

      1. 忘記 Unlock:最常見的錯誤。defer mu.Unlock() 是你的肌肉記憶。
      2. 複製已鎖定的鎖:會導致程序行為異常。
      3. 重入:Go 的鎖是不可重入的,自己鎖了自己,就會死鎖。這是設計哲學,不是 bug。

2. 併發編排 (WaitGroup)

  • 核心作用等待一組併發任務全部完成。
  • 精華

    • 比喻:就像田徑比賽。

      • wg.Add(N):裁判(主 goroutine)在發令槍響前,確認有 N 位選手就位。
      • go worker():發令槍響,N 位選手同時開跑。
      • wg.Done():每個選手跑過終點線時,舉手示意一下。
      • wg.Wait():裁判在終點線等着,直到所有 N 位選手都舉過手,他才宣佈比賽結束。
    • 決策口訣“併發啓動,等待所有,就用 WaitGroup。”

3. 單次執行 (Once) —— “一生一次的初始化”

  • 核心作用保證某個函數在整個程序生命週期內,無論被多少個 goroutine 調用,都只會被真正執行一次。
  • 你要記住的精華

    • 最經典用途實現線程安全的單例模式 (Singleton)。比如,全局的數據庫連接池、配置加載器,這些東西只需要初始化一次。
    • 比喻:就像一個“一次性封印”。第一個調用 once.Do(f) 的 goroutine 會執行 f,然後把封印貼上。所有後來者,無論多少,都會看到封印而直接跳過。
    • 注意:如果 f 函數執行失敗或 panic,這個封印依然被貼上了,once 會認為它“已經執行過”,不會再給你第二次機會。

4. 條件變量 (Cond) —— “等通知再開工”

  • 核心作用:讓一組 goroutine 在滿足某個特定條件之前,一直掛起等待,直到被喚醒。
  • 你要記住的精華

    • 極少使用! 這是 sync 包裏最不常用的工具。因為它的功能,99% 的情況下都可以用 Channel 更優雅、更清晰地實現。
    • 比喻:一羣工人在工位上睡覺 (cond.Wait())。一個工頭(另一個goroutine)在檢查條件(比如“原料到了嗎?”)。一旦條件滿足,工頭可以用 cond.Signal() 叫醒一個工人,或者用 cond.Broadcast() 叫醒所有工人起來幹活。
    • 決策口訣“想用 Cond 時,先想想能不能用 Channel。”

5. 臨時對象池 (Pool) —— “公用餐具回收站”

  • 核心作用複用內存中的臨時對象,以減輕 GC 壓力。
  • 你要記住的精華

    • 池化的是“工具”,不是“數據”。比如可被重置的 bytes.Buffer,而不是包含業務數據的用户信息。
    • 不保證存活:Pool 裏的對象隨時可能被 GC 回收,不能用它來做需要保證數據存在的緩存
    • 最佳場景:在高併發場景下,需要頻繁創建和銷燬的、小而多的臨時對象。
    • 決策口訣“GC 壓力大?看看是不是忘了用 sync.Pool。”

第二部分:Channel 與 Context —— Go 併發哲學的“靈魂”

掌握了它們,才算真正開始用“Go 的方式”去思考併發。

1. Channel —— “帶排隊功能的、線程安全的傳送帶”

  • 核心作用在 goroutine 之間安全地傳遞數據,並進行同步。 它完美地詮釋了 Go 的核心信條:“不要通過共享內存來通信,而要通過通信來共享內存。”
  • 精華

    • 比喻:把它想象成一條傳送帶

      • ch := make(chan int): 建造一條只能傳送 int 類型包裹的傳送帶。
      • ch <- data (發送): 把一個包裹放到傳送帶的入口
      • data := <- ch (接收): 從傳送帶的出口拿走一個包裹。
    • 兩種模式,兩種用途

      1. 無緩衝 Channel (make(chan int)) —— “同步接頭”

        • 特點:傳送帶沒有緩衝區。放包裹的人 (Writer) 和取包裹的人 (Reader) 必須同時到達傳送帶的兩端,完成“一手交一手”的交接。任何一方先到,都必須原地阻塞等待。
        • 核心用途同步 (Synchronization)。它不僅傳遞了數據,還保證了兩個 goroutine 在某個時間點上“碰頭”了。非常適合做任務編排信號通知
        • 決策口訣“需要等待、需要信號、需要同步,就用無緩衝 Channel。”
      2. 有緩衝 Channel (make(chan int, N)) —— “異步隊列”

        • 特點:傳送帶有N個槽位的緩衝區。只要緩衝區沒滿,放包裹的人 (Writer) 把包裹一放就可以立刻走人,去做別的事。只要緩衝區不空,取包裹的人 (Reader) 隨時來都能取到。
        • 核心用途解耦 (Decoupling) 和 流量削峯 (Buffering)。它允許生產者和消費者的處理速度不一致。比如生產者瞬間生產了100個任務扔進 Channel,消費者可以慢慢地、一個一個地取出來處理。
        • 決策口訣“生產者消費者,異步處理,流量緩衝,就用帶緩衝 Channel。”
    • 關閉 (close(ch))

      • 就像傳送帶的管理員按下了“停機”按鈕,並掛上牌子:“今天的包裹都送完了!”
      • 關閉後不能再發送(會 panic)。
      • 可以繼續接收,直到緩衝區裏的包裹被取完。之後再接收,會立刻得到該類型的零值
      • 這個特性常與 for range 結合,優雅地處理消費者退出。

2. select —— “併發任務的調度中心”

  • 核心作用同時監聽多個 Channel 的狀態,哪個先就緒就處理哪個。
  • 精華

    • 比喻:你是一個接線總機的話務員,面前有多個電話機 (Channel)

      • case <-ch1:: 1號電話響了。
      • case data := <-ch2:: 2號電話響了,並且有人説話了。
      • case ch3 <- data:: 你有一條消息要通過3號電話線發出去,並且對方正好接了。
    • 工作機制

      • 只選一個:select 會一直等着,直到其中至少一個電話機“就緒”(可以收或可以發)。
      • 隨機公平:如果多個電話同時響了,它會隨機接起其中一個,避免你總是先理睬1號線而餓死了2號線。
      • 永不阻塞 (帶default):如果加了 default 分支,select 就變成了一個“巡檢員”。它會立刻掃視一遍所有電話線,如果都沒響,它就不等了,直接去執行 default 的任務(比如喝口水)。這可以用來實現非阻塞的 channel 操作。
    • 決策口訣“多路監聽,超時控制,非阻塞收發,就用 select。”

3. Context —— “請求的生命週期控制器”

  • 核心作用在一個請求的完整生命週期內,傳遞“取消信號”、“超時期限”和“請求範圍的元數據”。
  • 精華

    • 比喻:它是一個“任務監督員”,跟着一個請求的整個處理流程。
    • 三大核心能力 (也是三大應用場景)

      1. 主動取消 (WithCancel):監督員配了一個“對講機”。你可以從外部通過 cancel() 函數對着對講機喊:“任務取消!” 所有帶着這個監督員的子任務(子 goroutine)都能聽到,然後安全退出。
      2. 超時控制 (WithTimeout, WithDeadline):監督員戴了一塊“倒計時手錶”。一旦時間到了,手錶會自動響鈴,效果等同於你喊了“任務取消!”。這是保護你的服務不被慢操作拖垮的生命線
      3. 傳遞元數據 (WithValue):監督員有一個“公文包”,可以攜帶只跟本次請求相關的數據(如 trace_id)。切記:不要用它來傳遞業務參數!
    • 黃金法則 (口訣)

      首參傳 Context,
      空值 Background。
      切莫長久存,
      Key 用自定義。
    • 級聯取消:Context 像一棵樹。砍掉樹根(取消父 Context),所有的樹枝和樹葉(子 Context)都會立即枯萎(被取消)。

sync vs. Channel 的終極選擇

任務編排用 Channel
共享資源保護用傳統併發原語 (sync 包)
  • 當問題是“一個數據如何被多個 goroutine 安全地讀寫”時 -> 想到 Mutex, RWMutex
  • 當問題是“多個 goroutine 如何按照一定順序協作,或者一個 goroutine 如何把工作成果交給另一個”時 -> 想到 Channel, select

第三部分總結:高級併發模式與擴展庫

這部分的核心是:知道在什麼場景下,標準庫的工具“不夠用”,以及此時應該拿出哪個更專業的工具來解決問題。

1. 原子操作 (sync/atomic) —— “無鎖編程的基石”

  • 核心作用:提供硬件級別的、不可中斷的原子性操作,比使用 Mutex 輕量得多
  • 精華

    • 比喻:它就像銀行櫃枱的“點鈔機”

      • Mutex:櫃員需要把整個櫃枱(臨界區)鎖起來,然後慢慢地手動數錢(讀-修改-寫),數完再解鎖。這個過程有上下文切換的開銷。
      • 原子操作:你直接把錢放進點鈔機 (atomic.AddInt64),機器“唰”地一下就完成了計數,這個過程是不可分割的,速度極快。
    • 什麼時候用?

      • 只針對“單個變量” 進行簡單的、併發安全的讀、寫、增、減操作。
      • 最經典的場景:實現高性能的併發計數器設置狀態標誌位
    • 核心函數

      • Add: 原子加/減。
      • Load: 原子讀。
      • Store: 原子寫。
      • Swap: 原子交換(寫入新值並返回舊值)。
      • CompareAndSwap (CAS): “看一眼再改”。先比較變量的當前值和期望值是否相等,如果相等,才寫入新值。這是實現各種無鎖數據結構(Lock-Free)的基石。
    • 決策口訣“保護單個值,簡單加減載,就用 atomic 代。”

2. errgroup —— “帶保險絲的 WaitGroup”

  • 核心作用:在 WaitGroup 的基礎上,增加了錯誤傳遞級聯取消的功能。
  • 精華

    • 解決了 WaitGroup 的兩大痛點

      1. WaitGroup 不知道 goroutine 是否出錯了。
      2. WaitGroup 無法在一個 goroutine 失敗後,通知其他 goroutine 停止工作。
    • 工作機制:它內部巧妙地結合了 WaitGroup, sync.Once 和 Context。

      • 啓動任務用 eg.Go()。
      • 只要任何一個 Go() 啓動的函數返回了 error,errgroup 會:

        1. 用 sync.Once 只記錄第一個出現的 error。
        2. 立即 cancel 它內部管理的那個 Context。
        3. 所有其他正在運行的、並且正確監聽了 ctx.Done() 的 goroutine,都會收到取消信號並退出。
    • 決策口訣“併發任務要連坐,一錯全停,就用 errgroup。”

3. singleflight —— “防擊穿的請求合併器”

  • 核心作用:對於同一個資源(由一個 key 標識)的併發讀請求,確保在同一時間內,只有一個請求會真正執行,其他請求都會原地等待,並共享這唯一一次執行的結果。
  • 精華

    • 完美解決“緩存擊穿”問題。這是它最最經典的應用場景。
    • 比喻:食堂打飯。

      • 沒有 singleflight:某個熱門菜(熱點Key)剛好沒了。100 個同學同時發現菜沒了,於是所有 100 個人都跑去後廚找廚師(查數據庫)。後廚瞬間被擠爆。
      • 有了 singleflight第一個發現菜沒了的同學,會獲得一個“催菜”的牌子,他代表大家去後廚。其他 99 個同學,則在原地排隊等待。等到那個同學把一大份新菜端回來,大家再一起分享。
    • 重要警告只能用於冪等的“讀”操作,絕對不能用於“寫”操作! 否則會導致嚴重的數據不一致問題。
    • 決策口訣“併發讀,防擊穿,合併請求,singleflight 擔。”

4. Worker Pool (協程池) —— “控制併發的閥門”

  • 核心作用:複用有限數量的 goroutine,去處理海量的任務,以防止因無限創建 goroutine 而耗盡系統資源。
  • 精華

    • 解決了什麼問題? Go 的 goroutine 雖然輕量,但也不是完全沒有成本。如果一瞬間來了 100 萬個任務,你直接 for 循環 go task(),你的程序會因為內存耗盡或調度壓力過大而崩潰。
    • 工作機制

      • 預先創建 N 個“工人” (worker goroutine)。
      • 所有任務都扔到一個“任務傳送帶” (通常是一個 channel) 上。
      • 這 N 個工人不斷地從傳送帶上取任務來執行。
      • 傳送帶滿了,提交任務的人就會被阻塞,形成自然的背壓 (Backpressure),保護了系統。
    • 決策口訣“任務海量,併發要控,Worker Pool 來疏通。”
    • 選型:在大多數場景下,使用一個成熟的第三方庫(如 gammazero/workerpool)比自己手寫更健壯。

第四部分總結:分佈式併發 —— 從“單機”到“集羣”

etcd 的核心是作為一個強一致的、高可用的“協調服務”。你可以把它想象成所有分佈式節點共同信任的、唯一的“公證處”“仲裁委員會”

1. etcd 的核心能力 (為什麼是它?)

  • Raft 共識算法:這是 etcd 的“靈魂”。它保證了所有對 etcd 的寫入操作,在集羣中的大多數節點都確認之前,是不會成功的。這確保了數據的強一致性
  • Key-Value 存儲:提供了一個簡單的、類似文件系統的 API (PUT, GET, DELETE)。
  • Watch 機制:允許客户端“訂閲”某個 Key 或目錄的變化。一旦數據發生改變,etcd 會立即主動通知所有訂閲者。
  • Lease (租約) 機制:允許一個 Key 和一個“租約”綁定。如果客户端在租約到期前沒有“續租”(發送心跳),etcd 會自動刪除這個 Key。這是實現分佈式鎖和節點健康檢查的基石。

etcd 在 Go 併發中的三大核心應用

1. 分佈式鎖 (Distributed Lock) —— “跨機器的 Mutex”

  • 核心作用:確保在整個分佈式集羣中,某個任務或對某個資源的訪問,在同一時間只被一個節點(進程)執行
  • 精華

    • 比喻:就像多個部門(分佈式節點)搶一個唯一的會議室(鎖)

      • 獲取鎖 (Lock):每個部門都去 etcd 這個“行政中心”,嘗試在 /locks/meeting_room 這個路徑下原子性地創建一個帶租約的 Key。etcd 保證了只有一個部門能創建成功。
      • 持有鎖:創建成功的部門,就獲得了會議室的使用權。它必須不斷地給自己的租約續期,證明會議還在開。
      • 釋放鎖 (Unlock):開完會,主動去 etcd 刪除那個 Key。
      • 節點宕機 (自動釋放):如果某個部門的服務器突然斷電了,它就無法再為租約續期。租約到期後,etcd 會自動刪除那個 Key,把會議室讓出來給別人用,完美地避免了死鎖
    • 讀寫鎖:etcd 的 concurrency 包也提供了讀寫鎖,其原理更復雜,但同樣實現了“寫優先”策略,防止寫操作被餓死。
    • 決策口訣“跨機搶資源,任務要獨佔,就用分佈式鎖幹。”

2. Leader 選舉 (Leader Election) —— “集羣不可一日無主”

  • 核心作用:在一組對等的服務節點中,選舉出唯一一個“主”節點 (Leader) 來負責某些特殊任務(比如處理寫請求、調度任務),其他節點則作為“從”節點 (Follower) 備用。並在主節點宕機後,能自動進行下一輪選舉
  • 精華

    • 實現方式:Leader 選舉本質上是對一個“分佈式鎖”的持續性爭搶

      • 所有節點都去 Campaign(競選)一個代表“王位”的 Key。
      • 第一個成功獲取到鎖(創建了帶租約的 Key)的節點,就成為了 Leader。
      • 其他節點則進入“等待”狀態,並用 Observe 機制監聽“王位”Key 的變化。
      • Leader 節點負責幹活,並不斷為自己的“王位”租約續期。
      • 如果 Leader 宕機,租約過期,Key 被自動刪除。
      • 其他等待的節點會立刻收到 Key 被刪除的通知,然後立刻開始下一輪的 Campaign,爭搶新的王位。
    • 決策口訣“主備要分明,宕機要切換,選舉 Leader 不出錯。”

3. 分佈式隊列 & 事務 (Queue & STM) —— “可靠的任務分發與原子操作”

  • 核心作用:提供跨機器的任務隊列和多 Key 操作的原子性保證。
  • 精華

    • 分佈式隊列 (Queue)

      • 特點可靠但低吞吐。每一次入隊和出隊,都是一次經過 Raft 共識的寫操作。
      • 適用場景低併發、但對任務分發的一致性和可靠性要求極高的場景。比如 Kubernetes 中,kube-controller-manager 把一個“創建 Pod”的任務放入隊列,必須保證這個任務不丟失,並且最終被某個 kubelet 消費。
      • 不適用絕對不能用它來做高吞吐量的業務消息隊列(比如訂單系統),那應該用 Kafka/RabbitMQ
    • 分佈式事務 (STM - Software Transactional Memory)

      • 解決了什麼? 原子性地修改多個 Key。比如,在 etcd 中實現的銀行轉賬,你需要原子性地“給A賬户的Key減100”並且“給B賬户的Key加100”。
      • 工作機制:採用樂觀鎖。事務提交時,etcd 會檢查你在事務期間讀取過的所有 Key 是否被其他人修改過。如果沒有,就成功提交;如果有,就整個事務失敗並自動重試
      • 決策口訣“跨 Key 改數據,要麼全成功,要麼全回滾,STM 來保證。”

最終總結:何時選擇分佈式併發原語?

這個決策非常簡單:

當你的業務邏輯,需要跨越多個獨立的進程或機器,來保證數據的一致性、操作的唯一性或角色的確定性時,你就必須使用分佈式併發原語。而在 Go 生態中,etcd 提供了最健壯、最地道的標準實現。
user avatar infodator 頭像
1 位用戶收藏了這個故事!

發佈 評論

Some HTML is okay.