吃透 Golang 基礎:基于共享變量的并發

文章目錄

  • sync.Mutex 互斥鎖
  • sync.RWMutex 讀寫鎖
  • sync.Once 惰性初始化
  • Goroutine 與線程
    • 動態棧
    • Goroutine 調度
    • GOMAXPROCS
    • Goroutine 沒有 ID 號

上一篇文章當中我們已經系統性地回顧了在 Go 當中基于 Goroutine 和 Channel 進行并發控制的方法,Goroutine 指的是 Golang 的應用級線程,相比于系統線程,goroutine 是輕量級的,完全在用戶態進行調度;Channel 可以被理解為 goroutine 之間進行通信的信道,它是 Golang 當中的引用類型,底層引用的數據結構是一個數組。基于 channel,在 Golang 當中我們可以直接進行 Goroutine 之間的通信,而無需基于共享內存進行通信。

這一節的內容是對 Golang 并發機制的深入,介紹了 Go 當中的鎖機制,基于鎖機制我們可以在 Go 當中基于共享變量來進行線程之間的通信。基于這一節的內容,我們將完整地回顧完與 Golang 并發機制以及 Goroutine 調度有關的知識,在這一節的最后,將會詳細地回顧 goroutine 與操作系統線程的區別。
在這里插入圖片描述

sync.Mutex 互斥鎖

基于 channel,我們實際上可以實現在同一時刻只有一個并發執行的 goroutine 訪問存儲關鍵資源的共享變量。下例模擬了一個銀行的存取邏輯,基于緩沖區大小為 1 的 channel 來確保不同 goroutine 對關鍵資源的訪問互斥:

var (sema =  make(chan struct{}, 1)	// 二元信號量balance int
)func Deposit(amount int) {sema <- struct{}{}			// 如果緩沖區為空, 就不會被阻塞balance = balance + amount<- sema						// 釋放鎖信號
}func Balance() int {sema <- struct{}{}			// 換言之, 如果緩沖區被填滿, 這條寫入操作就會被阻塞b := balance<- semareturn b
}

Golang 的sync包中有一個名為Mutex的類型同樣能夠實現上述邏輯,它具有兩個方法,分別是LockUnlock,前者會加鎖而后者會釋放鎖:

import "sync"var (mu      sync.Mutex // guards balancebalance int
)func Deposit(amount int) {mu.Lock()balance = balance + amountmu.Unlock()
}func Balance() int {mu.Lock()b := balancemu.Unlock()return b
}

LockUnlock之間代碼段的內容可以被當前持有鎖的 goroutine 隨意讀取或修改,這個代碼段叫做臨界區。鎖的持有者在其他 goroutine 獲取鎖之前需要調用Unlock,這也就意味著持有鎖的 goroutine 在結束之前必須將鎖釋放。

可以將Unlock行為與defer關鍵字組合,來確保鎖最終被釋放:

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

接下來我們研究一個更加復雜的案例,考慮下面的Withdraw函數,在成功時,它會調用Deposit扣減余額并返回 true,如果銀行資金不足,那么就恢復余額并返回 false:

func Withdraw(amount int) bool {Deposit(-amount)if Balance() < 0 {Deposit(amount)return false}return true
}

函數可以給出正確的結果,但這個函數有一個副作用,那就是過多的取款操作并發時,balance 可能會瞬間被減到 0,這可能會導致并發取款與支付被不合理的拒絕。產生上述問題的原因是,在當前的取款$ \rightarrow $支付邏輯不是一個原子性的操作,每一步都需要去單獨地獲取互斥鎖,任何一次上鎖都不會鎖住整個流程。

理想情況下,取款$ \rightarrow $支付邏輯應該在開始時獲取互斥鎖,結束時釋放,但由于我們還沒有修改Deposit/Balance的邏輯,這就意味著下述代碼會產生錯誤:

// ? INCORRECT
func Withdraw(amount int) bool {mu.Lock()defer mu.Unlock()Deposit(-amount)if Balance() < 0 {Deposit(amount)return false // insufficient funds}return true
}

Withdraw開始時,我們去獲取鎖,但是在Deposit中我們會再次嘗試獲取鎖,由于 Golang 的 Mutex 不可重入,無法對一個已經上鎖的 Mutex 再次加鎖,這就會導致程序死鎖,沒有辦法繼續執行下去(因為Deposit等待的鎖永遠不會釋放)。

基于上述原因,我們能夠做的就是對Deposit進行修改,新建一個它的非導出版本,在這個非導出版本當中,不需要基于鎖進行并發控制,因為它將會被Withdraw調用:

func Withdraw(amount int) bool {mu.Lock()defer mu.Unlock()deposit(-amount)if balance < 0 {deposit(amount)return false // insufficient funds}return true
}func Deposit(amount int) {mu.Lock()defer mu.Unlock()deposit(amount)
}func Balance() int {mu.Lock()defer mu.Unlock()return balance
}// This function requires that the lock be held.
func deposit(amount int) { balance += amount }

sync.RWMutex 讀寫鎖

上例當中的Balance函數實際的行為就是讀取變量的狀態,而不會對變量進行狀態改變,所以實際上我們并發調用多個Balance是安全的。

sync.RWMutex是一種特殊類型的鎖,它允許多操作并發執行,但是寫操作互斥,這種鎖叫做“多讀單寫”鎖:

var mu sync.RWMutex
var balance int
func Balance() int {mu.RLock()defer mu.RUnlock()return balance
}

sync.Once 惰性初始化

在單例模式當中,我們已經見到了「惰性初始化」的基本用法。一個線程安全的單例模式的模版是:

package mainimport ("fmt""sync"
)var lock sync.Mutextype singleton struct{}var instance *singletonfunc GetInstance() *singleton {lock.Lock()defer lock.Unlock()if instance == nil {return new(singleton)} else {return instance}
}func (s *singleton) SomeThing() {fmt.Println("SomeThing is called")
}func main() {s := GetInstance()s.SomeThing()
}

在“懶漢式”的單例模式下,為了確保單例類實例只有在需要的時候才被初始化,我們引入了一個 Mutex 鎖,來在調用GetInstance函數的時候,首先加鎖判斷單例類實例是否被創建,如果沒有被創建,則新建這個單例類實例。

我們進行進一步的細化,考慮下面這樣的一個 icons 變量:

var icons map[string]image.Image

我們嘗試使用“懶漢式”的做法來對 icons 進行初始化:

var mu sync.Mutex
var icons map[string]image.Imagefunc loadIcon(path string) image.Image {// ... ... ...return /* ... */
}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"),}
}// NOTE: not concurrency-safe!
func Icon(name string) image.Image {mu.Lock()defer mu.Unlock()if icons == nil {loadIcons() // one-time initialization}return icons[name]
}

上述做法下,icons 的初始化當然是安全的。如果有 goroutine 并發地調用Icon,由于鎖機制的存在,如果當前有其他 goroutine 正在調用Icon,那么當前Icon將會被阻塞。

一個問題在于,如果Icon已經被初始化完成,那么并發的 goroutine 無法并發地讀 icons 這個變量,這會導致性能的下降,我們可以進一步使用RLock來對上述Icon函數的邏輯進行修改:

var mu sync.RWMutex // guards icons
var icons map[string]image.Image
// Concurrency-safe.
func Icon(name string) image.Image {mu.RLock()if icons != nil {icon := icons[name]mu.RUnlock()return icon}mu.RUnlock()// acquire an exclusive lockmu.Lock()if icons == nil { // NOTE: must recheck for nilloadIcons()}icon := icons[name]mu.Unlock()return icon
}

修改后的Icon既在初始化時線程安全,又支持并發讀,但是代碼較為復雜,我們可以使用 Golang sync 包內置的sync.Once來專門解決這種“懶漢式”的一次性初始化的并發問題。

下例使用sync.Once繼續優化Icon的邏輯:

var loadIconsOnce sync.Once
var icons map[string]image.Image
func Icon(name string) image.Image{loadIconsOnce.Do(loadIcons)return icons[name]
}

每一次Do(loadIcons)的調用都會鎖定mutex,并檢查記錄初始化是否完成的 bool 變量。只有 bool 為 false 的時候才鎖定 mutex 來完成初始化操作。

總結一下,對于“懶漢式”初始化這種典型場景,為了確保并發調用該函數訪問單例類實例時,不會產生:

  1. 重復初始化;
  2. 讀等待;

可以使用sync.Once當中的Do來優化初始化邏輯。

Goroutine 與線程

在這一小節當中,我們具體地來區分一下 goroutine 這個用戶態線程與操作系統線程之間的區別。這些區別可以說是在面試 Golang 開發崗位時必知必會的細節。

動態棧

操作系統線程的棧有一個固定的內存塊(一般為 2MB),這個內存塊將會用作棧,這個棧用于存儲當前正在被調用的函數或掛起的函數(指的是為了調用當前這個函數而被暫時掛起的其他函數)的內部變量

2MB 對于一個線程的棧而言,很大又很小。與 goroutine 相比,初始的 goroutine 的棧內存大小僅為 2KB,2MB 是它的一千倍。

對于 Go 程序而言,同時創建成百上千個 goroutine 是很普遍的,如果每一個 goroutine 都需要這么大的內存用作棧的話,那么讓成百上千這個數量級的 goroutine 同時運行是不可能的。一個 goroutine 的棧是動態的,在其生命周期開始時,棧的大小只有 2KB。goroutine 的棧的作用與操作系統線程的棧類似,都是用于保存當前活躍以及掛起的函數調用的本地變量。goroutine 棧的大小可以動態伸縮,最大值可以達到 1TB,比傳統的固定大小的棧大得多得多,但實際上大多數情況下棧的內存空間不會達到這個數量級。

Goroutine 調度

操作系統線程會基于操作系統的內核進行調度。每幾毫秒,一個硬件的計時器會中斷處理器,調用一個名為 scheduler 的內核函數。這個函數會掛起當前執行的線程,并將它的寄存器內容保存到內存當中,檢查線程列表并決定下一次調度哪一個線程到 CPU 上執行,scheduler 會從內存中恢復該線程的寄存器信息,然后恢復該線程的現場并開始執行線程。

顯然,由于操作系統線程是被內核當中的 scheduler 函數調度的,所以一個線程向另一個線程移動需要進行完整的上下文切換(首先保存當前線程的上下文狀態到內存,之后檢查線程列表根據調度策略選擇下一個要調度的線程,從內存當中將它的狀態轉移到寄存器,恢復線程的現場,開始執行線程)。上下文切換的操作很慢,需要經過若干次的內存訪問,會增加 CPU 的運行周期。

Go 的運行時包含了自己的調度器(GMP 線程調度模型,Golang 開發面試時的考察熱點),比如m:n調度,它會讓n個操作系統線程多工調度m個 goroutine。Go 調度器的工作原理與內核當中的 scheduler 函數非常的相似,但是 Go 的調度發生在用戶態,而非內核態。由于不需要進入內核進行上下文切換,所以 Go 調度 goroutine 的成本比操作系統線程的調度成本要低很多。

GOMAXPROCS

Go 的調度器會使用一個名為 GOMAXPROCS 的變量來決定會有多少個 OS 線程同時執行 Go 代碼,其默認值是運行機器上的 CPU 的核數。比如對于一個 8 核的機器,調度器一次會在 8 個 OS 線程上調度 Go 代碼。

GOMAXPROCS 可以設置為比 CPU 核數更多的值,但是這樣做通常不會帶來性能的提升,甚至可能會由于過多的線程切換而導致性能下降。

GOMAXPROCS 對應的是 GMP 調度模型當中的 P,即具體的“調度器”,負責協調 G 和 M 的執行,GOMAXPROCS 的值就是 P 的數量。

有關 Golang 的 GMP 調度模型,可以詳見我之前的文章:https://blog.csdn.net/Coffeemaker88/article/details/146607091

Goroutine 沒有 ID 號

大多數支持多線程的操作系統或程序設計語言,都會為線程分配一個獨特的身份(ID),并且這個身份可以以一個普通值的形式(比如一個整型數值)被輕易地獲取到。基于線程的 ID,我們可以對線程進行本地存儲(Thread-Local Storage,TLS),只需要使用一個 map 將線程 ID 與實際的線程對應起來即可。

TLS 可能會被濫用,因為線程本身的身份信息可能會改變,這就會使得承載在這個線程上的函數的行為變得不可預測。因此直接在程序當中基于 TLS 與線程進行交互是不安全的。

Goroutine 沒有身份信息的概念,避免了 TLS 的濫用。

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

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

相關文章

智紳科技丨如何選擇一家好的養老機構?

居家養老、社區養老和機構養老是我們在養老相關消息中常常聽到的3個詞。在地方文件中&#xff0c;居家養老和社區養老還經常被統稱為居家社區養老或 社區居家養老。那么&#xff0c;這三者之間到底有什么不同呢&#xff1f; 居家養老服務涵蓋生活照料、家政服務、康復護理、醫…

【支持向量機】SVM線性支持向量機學習算法——軟間隔最大化支持向量機

支特向量機(support vector machines, SVM)是一種二類分類模型。它的基本模型是定義在特征空間上的間隔最大的線性分類器。包含線性可分支持向量機、 線性支持向量機、非線性支持向量機。 當訓練數據近似線性可分時&#xff0c;通過軟間隔最大化學習線性分類器&#xff0c; 即為…

面試 — 預準備 — 面試前準備攻略

好記憶不如爛筆頭&#xff0c;能記下點東西&#xff0c;就記下點&#xff0c;有時間拿出來看看&#xff0c;也會發覺不一樣的感受. 只講干貨&#xff0c;不羅里吧嗦&#xff01; 作為一個軟件從業者&#xff0c;在面試前的準備工作至關重要&#xff0c;能大幅提升你的求職成功…

Oracle停庫shutdown長時間無反應

Oracle停庫shutdown長時間無反應 現象:Oracle停庫卡住,長時間沒有反應。 SQL> shutdown immediate;注:此時切記不可Ctrl+C直接取消!切記不可Ctrl+C直接取消!切記不可Ctrl+C直接取消! 檢查alert_SID.log日志看是哪些會話進程導致的: Shutting down instance (immed…

使用ZYNQ芯片和LVGL框架實現用戶高刷新UI設計系列教程(第十八講

列表部件基本上是一個采用垂直布局的矩形&#xff0c;可向其中添加按鈕和文本。 部件包含&#xff1a; LV_PART_MAIN - 主要的屬性&#xff0c;大部分是這個部件。 LV_PART_SCROLLBAR - 滾動條的屬性。 &#xff08;1&#xff09; 添加文本 lv_obj_t * lv_list_add_text(lv_o…

Android Navigation 原理解析

1. nav_graph.xml 如何生成路由表 NavGraph 解析流程與原理 關鍵技術點&#xff1a; XML 解析&#xff1a; 使用 XmlResourceParser 解析 XML 文件 遍歷所有節點&#xff08;<fragment>, <activity>, <navigation>等&#xff09; Destination 創建&#…

HarmonyOS 應用權限管控流程

HarmonyOS 應用權限管控流程詳解 一、權限管控概述 HarmonyOS 通過多層次的安全機制保護用戶數據和系統資源&#xff0c;其中應用權限管控是核心組成部分。系統通過以下機制實現權限管控&#xff1a; 應用沙箱&#xff1a;每個應用運行在獨立沙箱中&#xff0c;通過TokenID識…

Python訓練營-Day33

import torch torch.cudaimport torch# 檢查CUDA是否可用 if torch.cuda.is_available():print("CUDA可用&#xff01;")# 獲取可用的CUDA設備數量device_count torch.cuda.device_count()print(f"可用的CUDA設備數量: {device_count}")# 獲取當前使用的C…

【STM32】中斷優先級管理 NVIC

這篇文章是對 Cortex-M3 內核中斷系統 和 STM32F1 系列 NVIC(嵌套向量中斷控制器) 的解析說明。我將從結構清晰、層次分明的角度,對 NVIC 中斷優先級分組的概念和 STM32F103 的實際情況做一個系統性的總結與敘述。 參考資料: STM32F1xx官方資料:《STM32中文參考手冊V10》…

Angular2--高級特性(TODO)

1 基礎 關于Angular的基礎部分&#xff0c;幾個核心部分和框架&#xff0c;在之前都寫過了。Angular1--Hello-CSDN博客 Angular的幾個核心部分和框架&#xff1a; 模板就是組件中的template&#xff0c;對應MVC的V。 組件類就是Component類&#xff0c;對應對應MVC的C。 服…

pikachu靶場通關筆記44 SSRF關卡02-file_get_content(三種方法滲透)

目錄 一、SSRF 1、簡介 2、原理 二、file_get_contents函數 1、功能 2、參數 3、返回值 4、file_get_contents與SSRF 三、滲透實戰 1、基本探測 2、http協議 &#xff08;1&#xff09;訪問upload-labs靶場 &#xff08;2&#xff09;訪問yijuhua.txt 3、file協議…

Android 控件 - EditText 的 Hint(Hint 基本用法、Hint 進階用法、單獨設置 Hint 的大小)

一、EditText 的 Hint 1、基本介紹 在 Android 開發中&#xff0c;EditText 的 Hint 用于顯示提示文本 提示文本當用戶沒有輸入任何內容時顯示&#xff0c;輸入內容后自動消失 2、基本使用 &#xff08;1&#xff09;在 XML 布局文件中設置 在 XML 布局文件中設置 Hint …

PostgreSQL(知識片):索引關聯度indexCorrelation

索引關聯度的絕對值越大&#xff0c;說明這個索引數據越好。絕對值最大為1。 首先我們創建一個表&#xff1a;tbl_corr&#xff0c;包含列&#xff1a;col、col_asc、col_desc、col_rand、data&#xff0c;col_asc存儲順序數據&#xff0c;col_desc存儲降序數據&#xff0c;col…

React純函數和hooks原理

純函數 JS 若滿足其下條件 &#xff0c;被稱為純函數 1。確定的輸入一定產生確定的輸出 2 不產生副作用 另外redux中的reducer也要求是純函數 Fiber 架構和hooks原理 useRef 在組件的整個聲明周期內保持不變 用法&#xff1a;1綁定dom元素 或者 綁定一個類組件 因為函數式…

養老專業實訓室虛擬仿真建設方案:助力人才培養與教育教學革新

隨著我國老齡化程度加深&#xff0c;養老服務行業人才需求激增。養老專業實訓室虛擬仿真建設方案憑借虛擬仿真技術&#xff0c;為養老專業教育教學帶來革新&#xff0c;對人才培養意義重大。點擊獲取實訓室建設方案 一、構建多元化虛擬場景&#xff0c;豐富實踐教學內容 模擬居…

LangChain 提示詞工程:語法結構詳解與完整實戰指南

LangChain 提示詞工程&#xff1a;語法結構詳解與完整實戰指南 我將為您系統性地解析 LangChain 中各類提示模板的核心語法結構&#xff0c;通過清晰展示語法與對應代碼示例&#xff0c;幫助您徹底掌握提示工程的實現方法。所有示例均圍繞報幕詞生成場景展開。 在這里插入圖片…

20250625解決在Ubuntu20.04.6LTS下編譯RK3588的Android14出現cfg80211.ko的overriding問題

Z:\14TB\versions\rk3588-android14-FriendlyElec\mkcombinedroot\res\vendor_modules.load 【拿掉/刪除這一項目&#xff01;】 cfg80211.ko 20250625解決在Ubuntu20.04.6LTS下編譯RK3588的Android14出現cfg80211.ko的overriding問題 2025/6/25 20:20 緣起&#xff1a;本文針對…

在WSL下搭建JavaWeb: JDBC學習環境

在WSL下搭建JavaWeb: JDBC學習環境 前言 ? 筆者最近打算放松一下&#xff0c;接觸一點經典的Java Web技術&#xff0c;自己在閑暇時間時玩一玩JavaWeb技術。這里開一個小系列整理一下最近學習的東西&#xff0c;以供參考和學習。 ? 筆者的計劃是使用VSCode寫代碼&#xff…

pscc系統如何部署,怎么更安全更便捷?

磐石云PSCC系統的安全高效部署需結合云原生架構與零信任安全模型&#xff0c;以下是經過大型項目驗證的部署方案及最佳實踐&#xff1a; 一、智能部署架構&#xff08;混合云模式&#xff09; 二、安全增強部署方案 1. 基礎設施安全 網絡隔離 采用 三層網絡分區&#xff1a;互…

協程驅動的高性能異步 HTTP 服務器:基礎實現與任務調度機制

一、引言&#xff1a;為什么用協程實現 HTTP 服務器&#xff1f; 傳統 HTTP 服務器的編程模型大致分為&#xff1a; 多線程阻塞型&#xff1a;每連接一線程&#xff0c;簡潔但擴展性差 事件驅動模型&#xff08;如 epoll 狀態機&#xff09;&#xff1a;高性能但邏輯復雜 回…