Go:使用共享變量實現并發

競態

在串行程序中,步驟執行順序由程序邏輯決定;而在有多個 goroutine 的并發程序中,不同 goroutine 的事件先后順序不確定,若無法確定兩個事件先后,它們就是并發的。若一個函數在并發調用時能正確工作,稱其為并發安全。當類型的所有可訪問方法和操作都是并發安全時,該類型為并發安全類型。并發安全的類型并非普遍存在,若要在并發中安全訪問變量,需限制變量僅在一個 goroutine 內存在,或維護更高層的互斥不變量。

package bankvar balance intfunc Deposit(amount int) { balance = balance + amount }
func Balance() int { return balance }// Alice:
go func() {bank.Deposit(200)  // A1fmt.Println("=", bank.Balance()) // A2
}()
// Bob:
go func() {bank.Deposit(100)  // B
}()

競態是指多個 goroutine 按交錯順序執行時,程序無法給出正確結果的情形。它對程序是致命的,可能潛伏在程序中,出現頻率低,且難以再現和分析。以銀行賬戶程序為例,在并發調用DepositBalance函數時,若多個 goroutine 交錯執行,可能出現數據競態,導致賬戶余額計算錯誤,如出現存款丟失等情況。數據競態發生在兩個或多個 goroutine 并發讀寫同一個變量,且至少其中一個是寫入時。當變量類型大于機器字長(如接口、字符串或 slice)時,數據競態問題會更復雜。

避免數據競態的方法

  • 不修改變量:對于延遲初始化的 map,若并發調用訪問可能存在數據競態。但如果在創建其他 goroutine 之前,用完整數據初始化 map 且不再修改,那么多個 goroutine 可安全并發調用相關函數讀取 map。
package bankvar deposits = make(chan int) // 發送存款額
var balances = make(chan int) // 接收余額func Deposit(amount int) { deposits <- amount }
func Balance() int { return <-balances }func teller() {var balance intfor {select {case amount := <-deposits:balance += amountcase balances <- balance:}}
}func init() {go teller() // 啟動監控goroutine
}
  • 避免多個 goroutine 訪問同一變量:通過將變量限制在單個 goroutine 內部訪問來避免競態。如 Web 爬蟲中主 goroutine 是唯一能訪問seen map 的,消息服務器中broadcaster goroutine 是唯一能訪問clients map 的。還可通過監控 goroutine 來限制對共享變量的訪問,如銀行案例中用teller goroutine 限制balance變量的并發訪問 。
  • 允許多個 goroutine 訪問,但同一時間只有一個可訪問:通過互斥機制實現。

互斥鎖:sync.Mutex

// 使用通道實現二進制信號量保護balance
var (sema    = make(chan struct{}, 1) // 用來保護 balance 的二進制信號量balance int
)
func Deposit(amount int) {sema <- struct{}{} // 獲取令牌balance = balance + amount<-sema // 釋放令牌
}
func Balance() int {sema <- struct{}{} // 獲取令牌b := balance<-sema // 釋放令牌return b
}

為保證同一時間最多有一個 goroutine 能訪問共享變量,可使用容量為 1 的通道作為二進制信號量。

由于互斥鎖模式應用廣泛,Go 語言sync包提供了Mutex類型來支持這種模式,Lock方法用于獲取令牌(上鎖),Unlock方法用于釋放令牌(解鎖)。

// 使用sync.Mutex實現互斥鎖保護balance
import "sync"
var (mu      sync.Mutex // 保護 balancebalance int
)
func Deposit(amount int) {mu.Lock()balance = balance + amountmu.Unlock()
}
func Balance() int {mu.Lock()b := balancemu.Unlock()return b
}

示例:以銀行賬戶程序為例,定義musync.Mutex類型來保護balance變量 。在DepositBalance函數中,通過先調用mu.Lock()獲取互斥鎖,訪問或修改balance變量,最后調用mu.Unlock()釋放鎖 ,確保共享變量不會被并發訪問 。這種函數、互斥鎖、變量的組合方式稱為監控(monitor)模式。

func Balance() int {mu.Lock()defer mu.Unlock()return balance
}

LockUnlock之間的代碼區域稱為臨界區域,此區域內可自由讀寫共享變量 。一個 goroutine 在使用完互斥鎖后應及時釋放,對于有多個分支(尤其是錯誤分支)的復雜函數,可使用defer語句延遲執行Unlock,將臨界區域擴展到函數結尾,保證鎖能正確釋放 ,即使在臨界區域崩潰時也能正常執行解鎖操作 。

原子操作與互斥鎖的應用

// 不正確的Withdraw實現示例
func Withdraw(amount int) bool {Deposit(-amount)if Balance() < 0 {Deposit(amount)return false // 余額不足}return true
}// 錯誤的Withdraw加鎖嘗試示例
func Withdraw(amount int) bool {mu.Lock()defer mu.Unlock()Deposit(-amount)if Balance() < 0 {Deposit(amount)return false // 余額不足}return true
}// 正確的Withdraw實現示例
func Withdraw(amount int) bool {mu.Lock()defer mu.Unlock()deposit(-amount)if balance < 0 {deposit(amount)return false // 余額不足}return true
}func Deposit(amount int) {mu.Lock()defer mu.Unlock()deposit(amount)
}func Balance() int {mu.Lock()defer mu.Unlock()return balance
}// 這個函數要求已獲取互斥鎖
func deposit(amount int) { balance += amount }

Withdraw函數為例,最初版本因不是原子操作(包含多個串行操作且未對整個操作上鎖)存在問題,在嘗試超額提款時可能導致余額異常 。改進版本應在整個操作開始時申請一次互斥鎖 ,但直接在Withdraw中嵌套調用已使用互斥鎖的Deposit函數會因互斥鎖不可再入導致死鎖 。最終解決方案是將Deposit函數拆分為不導出的deposit函數(假定已獲取互斥鎖并完成業務邏輯)和導出的Deposit函數(負責獲取鎖并調用deposit ),從而正確實現Withdraw函數 。使用互斥鎖時,應確保互斥鎖本身及被保護的變量都不被導出 ,以維持并發中的不變性

讀寫互斥鎖:sync.RWMutex

var mu sync.RWMutex
var balance intfunc Balance() int {mu.RLock() // 讀鎖defer mu.RUnlock()return balance
}

以 Bob 頻繁查詢賬戶余額為例,銀行的Balance函數只是讀取變量狀態,多個Balance請求可并發運行,只要DepositWithdraw請求不同時運行即可 。為滿足這種場景需求,需要一種特殊的鎖,即多讀單寫鎖,Go 語言中的sync.RWMutex可提供此功能。

  • 讀鎖操作:定義musync.RWMutex類型 ,在Balance函數中,通過調用mu.RLock()獲取讀鎖(共享鎖),使用defer mu.RUnlock()延遲釋放讀鎖,確保在函數結束時釋放鎖 ,這樣多個讀操作可并發進行。
  • 寫鎖操作Deposit函數等寫操作函數,仍通過調用mu.Lock()獲取寫鎖(互斥鎖),mu.Unlock()釋放寫鎖 ,保證寫操作時的獨占訪問權限。

注意事項

  • RLock僅適用于臨界區域內對共享變量無寫操作的情形 ,因為有些看似只讀的函數可能會更新內部變量,若不確定應使用獨占版本的Lock
  • 當絕大部分 goroutine 都在獲取讀鎖且鎖競爭激烈時,RWMutex才有優勢,因為其內部簿記工作更復雜,在競爭不激烈時比普通互斥鎖慢

內存同步

以銀行賬戶的Balance函數為例,其需要互斥鎖不僅是防止操作交錯,還涉及內存同步問題。現代計算機多處理器有本地內存緩存,寫操作先緩存在處理器中,刷回內存順序可能與 goroutine 寫入順序不一致。通道通信、互斥鎖等同步原語可使處理器將累積寫操作刷回內存并提交,保證執行結果對其他處理器上的 goroutine 可見。

var x, y int
go func() {x = 1fmt.Print("y:", y, " ")
}()
go func() {y = 1fmt.Print("x:", x, " ")
}()

通過代碼示例,兩個 goroutine 并發訪問共享變量xy,在未使用互斥鎖時存在數據競態,預期輸出為y:0 x:1x:0 y:1x:1 y:1y:1 x:1這四種情況之一 。但實際可能出現x:0 y:0y:0 x:0這種意外輸出 。原因在于單個 goroutine 內語句執行順序一致,但在無同步措施時,不同 goroutine 間無法保證事件順序一致 。編譯器可能因賦值和打印對應不同變量,交換語句執行順序,CPU 也可能因緩存等問題導致一個 goroutine 的寫入操作對另一個 goroutine 的Print語句不可見 。

解決:為避免這些并發問題,可采用成熟模式,將變量限制在單個 goroutine 中;對于其他變量,使用互斥鎖進行同步 。

延遲初始化sync.Once

var icons map[string]image.Image
func loadIcons() {icons = map[string]image.Image{"spades.png":  loadIcon("spades.png"),"hearts.png":  loadIcon("hearts.png"),"diamonds.png": loadIcon("diamonds.png"),"clubs.png":   loadIcon("clubs.png"),}
}
// 并發不安全版本
func Icon(name string) image.Image {if icons == nil {loadIcons() // 一次性地初始化}return icons[name]
}

延遲昂貴的初始化步驟到實際需要時進行,可避免增加程序啟動延時。以icons變量為例,初始版本在Icon函數中檢測icons是否為空,若為空則調用loadIcons進行一次性初始化 ,但此方式在并發調用Icon時不安全。

var mu sync.Mutex // 保護 icons
var icons map[string]image.Image// 并發安全版本(使用普通互斥鎖)
func Icon(name string) image.Image {mu.Lock()defer mu.Unlock()if icons == nil {loadIcons()}return icons[name]
}var mu sync.RWMutex // 保護 icons
var icons map[string]image.Image// 并發安全版本(使用讀寫互斥鎖)
func Icon(name string) image.Image {mu.RLock()if icons!= nil {icon := icons[name]mu.RUnlock()return icon}mu.RUnlock()mu.Lock()if icons == nil { // 必須重新檢查nil值loadIcons()}icon := icons[name]mu.Unlock()return icon
}

在無顯式同步情況下,編譯器和 CPU 可能重排loadIcons語句執行順序,導致一個 goroutine 發現icons不為nil時,初始化可能尚未真正完成 。使用互斥鎖可解決同步問題,如用sync.Mutex保護icons變量 ,但這會限制并發訪問,即使初始化完成且不再更改,也會阻止多個 goroutine 并發讀取 。使用sync.RWMutex雖能改善并發讀問題,但代碼復雜且易出錯 。

var loadIconsOnce sync.Once
var icons map[string]image.Image// 并發安全版本(使用sync.Once)
func Icon(name string) image.Image {loadIconsOnce.Do(loadIcons)return icons[name]
}

sync.Once為一次性初始化問題提供簡化方案 。它包含布爾變量記錄初始化是否完成,以及互斥量保護相關數據 。OnceDo方法以初始化函數為參數 ,首次調用Do時,鎖定互斥量并檢查布爾變量,若為假則調用初始化函數并將變量設為真,后續調用相當于空操作 。通過使用sync.Once,可確保變量在正確構造之前不被其他 goroutine 訪問,避免競態問題 。

競態檢測器

Go 語言運行時和工具鏈提供競態檢測器,用于檢測并發編程中的數據競態問題。在go buildgo rungo test命令中添加-race參數即可啟用 。啟用后,編譯器會構建修改后的版本,記錄運行時對共享變量的訪問,以及讀寫變量的 goroutine 標識,還會記錄同步事件(如go語句、通道操作、互斥鎖調用、WaitGroup調用等 )。

競態檢測器通過研究事件流,找出一個 goroutine 寫入變量后,無同步操作時另一個 goroutine 讀寫該變量的情況,即數據競態 。檢測到競態后,會輸出包含變量標識、讀寫 goroutine 調用棧的報告,幫助定位問題 。

它只能檢測運行時發生的競態,無法保證程序絕對不會發生競態 。為獲得最佳檢測效果,測試應包含并發使用包的場景 。由于增加了額外簿記工作,帶競態檢測功能的程序運行時需更長時間和更多內存,但對于排查不常發生的競態,能節省大量調試時間 。

goroutine 和線程

可增長的棧

每個 OS 線程都有固定大小的棧內存,通常為 2MB ,用于保存在函數調用期間正在執行或臨時暫停函數中的局部變量。但這個固定大小存在弊端,對于簡單的 goroutine(如僅等待WaitGroup或關閉通道 ),2MB 棧內存浪費;對于復雜深度遞歸函數,固定大小棧又不夠用,且無法兼顧空間效率和支持更深遞歸。

goroutine 在生命周期開始時棧很小,典型為 2KB ,也用于存放局部變量。與 OS 線程不同,goroutine 的棧可按需增大和縮小,大小限制可達 1GB ,比線程棧大幾個數量級,能更靈活適應不同場景,極少的 goroutine 才會用到這么大棧。

goroutine調度

OS 線程由 OS 內核調度。每隔幾毫秒,硬件時鐘中斷觸發 CPU 調用調度器內核函數 。該函數暫停當前運行線程,保存寄存器信息到內存,選擇下一個運行線程,恢復其注冊表信息后繼續執行 。此過程涉及完整上下文切換,包括保存和恢復線程狀態、更新調度器數據結構,因內存訪問及 CPU 周期消耗,操作較慢 。

Go 運行時有自己的調度器,采用 m:n 調度技術(將 m 個 goroutine 復用 / 調度到 n 個 OS 線程 )。與內核調度器不同,Go 調度器不由硬件時鐘定期觸發,而是由特定 Go 語言結構觸發 ,如 goroutine 調用time.Sleep、被通道阻塞或進行互斥量操作時,調度器將其設為休眠模式,轉而運行其他 goroutine,直到可喚醒該 goroutine 。由于無需切換到內核語境,調度 goroutine 成本比調度線程低很多 。

GOMAXPROCS

Go 調度器通過GOMAXPROCS參數確定同時執行 Go 代碼所需的 OS 線程數量 ,默認值為機器上的 CPU 數量 。例如在 8 核 CPU 機器上,調度器會將 Go 代碼調度到 8 個 OS 線程上執行(它是 m:n 調度中的 n )。處于休眠、被通道阻塞的 goroutine 不占用線程,阻塞在 I/O 及系統調用或調用非 Go 語言函數的 goroutine 雖需獨立 OS 線程,但該線程不計入GOMAXPROCS

for {go fmt.Print(0)fmt.Print(1)
}
// $ GOMAXPROC=1 go run hacker-cliche.go  11111111111111111118008000000000000001111...
// $ GOMAXPROCS=2 go run hacker-cliche.go 01010101010101010101100110010101101001010...

可通過GOMAXPROCS環境變量或runtime.GOMAXPROCS函數顯式控制該參數 。文中通過一個不斷輸出 0 和 1 的小程序示例展示其效果 ,當GOMAXPROCS=1時,每次最多一個 goroutine 運行,主 goroutine 和輸出 0 的 goroutine 交替執行;當GOMAXPROCS=2時,兩個 goroutine 可同時運行 。由于影響 goroutine 調度因素眾多且運行時不斷變化,實際結果可能不同。

goroutine沒有標識

在多數支持多線程的操作系統和編程語言中,當前線程有獨特標識,通常為整數或指針 。利用此標識可構建線程局部存儲,即一個以線程標識為鍵的全局 map,使每個線程能獨立存儲和獲取值,不受其他線程干擾 。

goroutine 沒有可供程序員訪問的標識 ,這是設計選擇。因為線程局部存儲易被濫用,如 Web 服務器使用支持線程局部存儲的語言時,很多函數通過訪問該存儲查找 HTTP 請求信息,會導致類似過度依賴全局變量的 “超距作用”,使函數行為不僅取決于參數,還與運行線程標識有關,在需要改變線程標識(如使用工作線程 )時,函數行為會變得不可預測 。

Go 語言鼓勵簡單編程風格,函數行為應僅由顯式指定參數決定,這樣程序更易閱讀,且在將函數子任務分發到多個 goroutine 時,無需考慮 goroutine 標識問題 。

參考資料:《Go程序設計語言》

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

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

相關文章

Vue3 SSR Serverless架構革命:彈性計算與量子加速

一、全維度Serverless SSR架構 1.1 蜂巢式彈性調度系統 1.2 冷啟動時間優化表 優化策略Node.js冷啟(ms)Deno冷啟(ms)Bun冷啟(ms)裸啟動1800960420預編譯二進制650380210內存快照預熱22016090WASM實例池15011075量子狀態預載453832 二、邊緣渲染協議升級 2.1 流式SSR響應協議…

FPAG IP核調用小練習

一、調用步驟 1、打開Quartus 右上角搜索ROM&#xff0c;如圖所示 2、點擊后會彈出如圖所示 其中文件路徑需要選擇你自己的 3、點擊OK彈出如圖所示 圖中紅色改為12與1024 4、然后一直點NEXT&#xff0c;直到下圖 這里要選擇后綴為 .mif的文件 5、用C語言生成 .mif文件 //…

Spring Cloud 服務間調用深度解析

前言 在構建微服務架構時&#xff0c;服務間的高效通信是至關重要的。Spring Cloud 提供了一套完整的解決方案來實現服務間的調用、負載均衡、服務發現等功能。本文將深入探討 Spring Cloud 中服務之間的調用機制&#xff0c;并通過源碼片段和 Mermaid 圖表幫助讀者更好地理解…

AF3 generate_chain_data_cache腳本解讀

AlphaFold3 generate_chain_data_cache 腳本在源代碼的scripts文件夾下。該腳本從指定目錄中批量解析 mmCIF/PDB 文件的工具,并將每個鏈的基本信息(序列、分辨率、是否屬于聚類等)提取并寫入 JSON 文件,主要用于后續蛋白質建模、過濾或訓練數據準備。 源代碼: import ar…

vue項目打包部署到maven倉庫

需要的資源文件&#xff0c;都放在根目錄下&#xff1a; 1. versionInfo.js const fs require(fs) const path require(path) const mkdirp require(mkdirp) const spawn require(child_process).spawnconst packageObj require(./package.json) const versionNo packa…

MegaTTS3: 下一代高效語音合成技術,重塑AI語音的自然與個性化

在近期的發布中&#xff0c;浙江大學趙洲教授團隊與字節跳動聯合推出了革命性的第三代語音合成模型——MegaTTS3&#xff0c;該模型不僅在多個專業評測中展現了卓越的性能&#xff0c;還為AI語音的自然性和個性化開辟了新的篇章。 MegaTTS3技術亮點 零樣本語音合成 MegaTTS3采用…

【教程】PyTorch多機多卡分布式訓練的參數說明 | 附通用啟動腳本

轉載請注明出處&#xff1a;小鋒學長生活大爆炸[xfxuezhagn.cn] 如果本文幫助到了你&#xff0c;歡迎[點贊、收藏、關注]哦~ 目錄 torchrun 一、什么是 torchrun 二、torchrun 的核心參數講解 三、torchrun 會自動設置的環境變量 四、torchrun 啟動過程舉例 機器 A&#…

計算機視覺——基于 Yolov8 目標檢測與 OpenCV 光流實現目標追蹤

1. 概述 目標檢測&#xff08;Object Detection&#xff09;和目標追蹤&#xff08;Object Tracking&#xff09;是計算機視覺中的兩個關鍵技術&#xff0c;它們在多種實際應用場景中發揮著重要作用。 目標檢測指的是在靜態圖像或視頻幀中識別出特定類別的目標對象&#xff0…

MySQL——流程控制

一、IF條件語句 語法 IF condition THENstatements; ELSEIF condition THENstatements; ELSEstatements; END IF; 判斷成績等級 # 判斷成績等級 # 輸入學生的編號,取出學生的第一門課&#xff0c;然后判斷當前的課程的等級 drop procedure if exists p2; delimiter $$ crea…

C# + Python混合開發實戰:優勢互補構建高效應用

文章目錄 前言&#x1f94f;一、典型應用場景1. 桌面應用智能化2. 服務端性能優化3. 自動化運維工具 二、四大技術實現方案方案1&#xff1a;進程調用&#xff08;推薦指數&#xff1a;★★★★☆&#xff09;方案2&#xff1a;嵌入Python解釋器&#xff08;推薦指數&#xff1…

MLflow 入門

官方主頁 MLflow | MLflow官方文檔 MLflow: A Tool for Managing the Machine Learning Lifecycle | MLflow 0. 簡介 MLflow 是一個開源平臺&#xff0c;專門為了幫助機器學習的從業者和團隊處理機器學習過程中的復雜性而設計。MLflow 關注機器學習項目的完整生命周期&#x…

【藍橋杯選拔賽真題101】Scratch吐絲的蜘蛛 第十五屆藍橋杯scratch圖形化編程 少兒編程創意編程選拔賽真題解析

目錄 scratch吐絲的蜘蛛 一、題目要求 1、準備工作 2、功能實現 二、案例分析 1、角色分析 2、背景分析 3、前期準備 三、解題思路 四、程序編寫 五、考點分析 六、推薦資料 1、scratch資料 2、python資料 3、C++資料 scratch吐絲的蜘蛛 第十五屆青少年藍橋杯s…

智譜最新模型GLM4是如何練成的

寫在前面 這篇博客將基于《ChatGLM: A Family of Large Language Models from GLM-130B to GLM-4 All Tools》,深入剖析 GLM-4 系列在**模型架構設計、預訓練、后訓練(對齊)、以及關鍵技術創新(如長上下文處理、Agent 能力構建)**等環節的實現邏輯與設計考量,帶你全面了…

第二屆電氣技術與自動化工程國際學術會議 (ETAE 2025)

重要信息 2025年4月25-27日 中國廣州 官網: http://www.icetae.com/ 部分 征稿主題 Track 1&#xff1a;電氣工程 輸配電、電磁兼容、高電壓和絕緣技術、電氣工程、電氣測量、電力電子及其應用、機電一體化、電路與系統、電能質量和電磁兼容性、電力系統及其自…

設備調試--反思與總結

最近回顧項目&#xff0c; 發現&#xff1a;在調試過程中最耽誤時間的可能不是技術難度&#xff0c;而是慣性思維&#xff1b; 例如&#xff1a; 我寫can通信濾波器的時候&#xff0c;可能是不過濾的&#xff1b;是接收所有的id報文&#xff0c;然后用業務邏輯過濾&#xff08…

C++項目:高并發內存池_下

目錄 8. thread cache回收內存 9. central cache回收內存 10. page cache回收內存 11. 大于256KB的內存申請和釋放 11.1 申請 11.2 釋放 12. 使用定長內存池脫離使用new 13. 釋放對象時優化成不傳對象大小 14. 多線程環境下對比malloc測試 15. 調試和復雜問題的調試技…

深度學習入門:神經網絡的學習

目錄 1 從數據中學習1.1 數據驅動1.2 訓練數據和測試數據 2損失函數2.1 均方誤差2.2 交叉熵誤差2.3 mini-batch學習2.4 mini-batch版交叉熵誤差的實現2.5 為何要設定損失函數 3 數值微分3.1 數值微分3.3 偏導數 4 梯度4.1 梯度法4.2 神經網絡的梯度 5 學習算法的實現5.1 2層神經…

【第45節】windows程序的其他反調試手段上篇

目錄 引言 一、通過窗口類名和窗口名判斷 二、檢測調試器進程 三、父進程是否是Explorer 四、RDTSC/GetTickCount時間敏感程序段 五、StartupInfo結構的使用 六、使用BeingDebugged字段 七、 PEB.NtGlobalFlag,Heap.HeapFlags,Heap.ForceFlags 八、DebugPort:CheckRem…

Golang|select

文章目錄 多路監聽超時控制 多路監聽 如果selcet外面沒有for循環&#xff0c;則只會監聽一次&#xff0c;要實現一直監聽的話要加for循環但是如果要設置退出條件的話&#xff0c;break語句只會退出這個select而不會退出for循環 select也可以有default&#xff0c;用于不用等cha…

無人機的群體協同與集群控制技術要點!

一、技術要點 通信技術 高效可靠的通信鏈路&#xff1a;無人機集群需要穩定、低延遲的通信網絡&#xff0c;以實現實時數據傳輸和指令交互。通信方式包括無線自組織網絡&#xff08;Ad Hoc&#xff09;、蜂窩網絡、衛星通信等&#xff0c;需根據任務場景選擇合適的通信技術。…