go語言的鎖

本篇文章主要講鎖,主要會涉及go的sync.Mutex和sync.RWMutex。

一.鎖的概念和發展

1.1 鎖的概念

所謂的加鎖和解鎖其實就是指一個數據是否被占用了,通過Mutex內的一個狀態來表示。

例如,取 0 表示未加鎖,1 表示已加鎖;

  • 上鎖:把 0 改為 1;
  • 解鎖:把 1 置為 0.
  • 上鎖時,假若已經是 1,則上鎖失敗,需要等鎖的主人解鎖,將狀態改為 0,才可以被其他鎖鎖上。

這就是一個鎖的基本骨架,鎖主要就是加鎖和解鎖兩個狀態。

并且這里要注意一個點,就是這兩個操作具有原子性,不可以被拆解。

1.2 由自旋等阻塞的升級過程

一個優先的工具需要具備探測并適應環境,從而采取不同對策因地制宜的能力.

針對 goroutine 加鎖時發現鎖已被搶占的這種情形,此時擺在面前的策略有如下兩種:

  • 阻塞/喚醒:將當前 goroutine 阻塞掛起,直到鎖被釋放后,以回調的方式將阻塞 goroutine 重新喚醒,進行鎖爭奪;
  • 自旋 + CAS:基于自旋結合 CAS 的方式,重復校驗鎖的狀態并嘗試獲取鎖,始終把主動權握在手中.

阻塞和喚醒大家肯定都知道,這里主要說一下自旋和CAS

如果看過gmp的同學對自旋肯定不陌生,所謂的自旋其實就是輪詢,什么意思?這里舉一個例子:

比如所謂的主動輪詢,其實就是指如果加鎖失敗之后,它會停歇一會,然后再次詢問,我可以加鎖嘛?如果可以,就取到這個鎖,要是不可以,就進行下一次的輪詢。

和阻塞/喚醒不同,他是需要等待通知,說這把鎖釋放了,然后后續的goroutine才可以拿到這把鎖。

CAS是什么?

CAS全稱為Compare-And-Swap,是一種原子操作,用于多線程編程中實現無鎖同步。

上述的方案各有各的優缺點,都有其對應的適用場景,接下來來看一下

鎖競爭方案

優勢

劣勢

適用場景

阻塞/喚醒

精準打擊,不浪費 CPU 時間片

需要掛起協程,進行上下文切換,操作較重

并發競爭激烈的場景

自旋+CAS

無需阻塞協程,短期來看操作較輕

長時間爭而不得,會浪費 CPU 時間片

并發競爭強度低的場景

這里對這兩種鎖思想做一個介紹吧:

阻塞/喚醒:這種形式被稱為是悲觀鎖,當G獲取鎖失敗而阻塞時,會被掛起,標記為waiting的狀態,主動讓出Processor,直接讓M和G結合,而P去執行其他的G(保證不會浪費這個P)鎖被釋放之后,才會喚醒G

自旋+CAS:這種形式被稱為是樂觀鎖,主動權掌握在自己的手中(也就是不釋放processor),會不斷主動輪詢嘗試獲取這個鎖

而sync.Mutex結合了上述的兩種方案,指定了一個鎖升級的過程,讓我們來看看吧

進行了一個怎么樣的鎖升級?

其實就是設計了一個狀態的轉化,由樂觀轉換為悲觀,為什么要這樣設計呢?

先來說說具體的方法:

  • 首先保持樂觀,goroutine 采用自旋 + CAS 的策略爭奪鎖;
  • 嘗試持續受挫達到一定條件后,判定當前過于激烈,則由自旋轉為 阻塞/掛起模式.

這樣做的原因是可以具備探測和適應環境,因地制宜采取不同的策略,首先采用樂觀的狀態,如果幾次自旋無果,就認為現在是并發激烈的情況,就會轉化為悲觀的狀態。

1.3 饑餓模式

上一小節的升級策略主要是面向性能,而本小節引入的饑餓模式,則是對公平性問題的探討。

下面首先拎清兩個概念:

  • 饑餓:顧名思義,是因為非公平機制的原因,導致 Mutex 阻塞隊列中存在 goroutine 長時間取不到鎖,從而陷入饑荒狀態;
  • 饑餓模式:當 Mutex 阻塞隊列中存在處于饑餓態的 goroutine 時,會進入模式,將搶鎖流程由非公平機制轉為公平機制.

Mutex運作下的兩種模式

  • 正常模式/非饑餓模式:這是 sync.Mutex 默認采用的模式. 當有 goroutine 從阻塞隊列被喚醒時,會和此時先進入搶鎖流程的 goroutine 進行鎖資源的爭奪,假如搶鎖失敗,會重新回到阻塞隊列頭部.

這里雖然有一個阻塞隊列,當鎖資源被釋放,按理說阻塞隊列的隊首的G或獲取這個鎖資源,這其實是很公平了,但是實際上他只是看似公平,因為還有沒進阻塞隊列的G,還記得什么時候進阻塞隊列嘛?對,就是當自旋結束才會進,這樣一來就很清晰了,隊首的G會和自旋的G搶占這個鎖,如果說隊首的G排了半天隊,結果被這個初出茅廬的自旋G搶了鎖資源,這還叫公平嘛?結果顯而易見,肯定是不公平的,于是為了解決這個問題,就有了饑餓模式。

(值得一提的是,此時被喚醒的老 goroutine 相比新 goroutine 是處于劣勢地位,因為新 goroutine 已經在占用 CPU 時間片,且新 goroutine 可能存在多個,從而形成多對一的人數優勢,因此形勢對老 goroutine 不利.)

  • 饑餓模式:這是 sync.Mutex 為拯救陷入饑荒的老 goroutine 而啟用的特殊機制,饑餓模式下,鎖的所有權按照阻塞隊列的順序進行依次傳遞. 新 goroutine 進行流程時不得搶鎖,而是進入隊列尾部排隊.

這樣就可以避免自旋的鎖搶占鎖資源了

兩種模式的轉化

  • 默認為正常模式;
  • 正常模式 -> 饑餓模式:當阻塞隊列存在 goroutine 等鎖超過 1ms 而不得,則進入饑餓模式;
  • 饑餓模式 -> 正常模式:當阻塞隊列已清空,或取得鎖的 goroutine 等鎖時間已低于 1ms 時,則回到正常模式.

小結:正常模式靈活機動,性能較好;饑餓模式嚴格死板,但能捍衛公平的底線. 因此,兩種模式的切換體現了 sync.Mutex 為適應環境變化,在公平與性能之間做出的調整與權衡. 回頭觀望,這一項因地制宜、隨機應變的能力正是許多優秀工具所共有的特質.

二.sync.Mutex

在這之前呢,做一個簡單的補充,在sync下,提供了一個接口,提供了一個實現屬于自己的鎖的方法哦

type Locker interface {Lock()Unlock()
}

2.1 核心數據結構

type Mutex struct {state int32sema  uint32
}
  • state:鎖中最核心的狀態字段,不同 bit 位分別存儲了 mutexLocked(是否上鎖)、mutexWoken(是否有 goroutine 從阻塞隊列中被喚醒)、mutexStarving(是否處于饑餓模式)的信息,具體在 2.2 節詳細展開;
  • sema:用于阻塞和喚醒 goroutine 的信號量.

const (mutexLocked = 1 << iota // mutex is lockedmutexWokenmutexStarvingmutexWaiterShift = iotastarvationThresholdNs = 1e6
)
  • mutexLocked = 1:state 最右側的一個 bit 位標志是否上鎖,0-未上鎖,1-已上鎖;
  • mutexWoken = 2:state 右數第二個 bit 位標志是否有 goroutine 從阻塞中被喚醒,0-沒有,1-有;
  • mutexStarving = 4:state 右數第三個 bit 位標志 Mutex 是否處于饑餓模式,0-非饑餓,1-饑餓;
  • mutexWaiterShift = 3:右側存在 3 個 bit 位標識特殊信息,分別為上述的 mutexLocked、mutexWoken、mutexStarving;
  • starvationThresholdNs = 1 ms:sync.Mutex 進入饑餓模式的等待時間閾值.

2.2 state字段

低 3 位分別標識 mutexLocked(是否上鎖)、mutexWoken(是否有協程在搶鎖)、mutexStarving(是否處于饑餓模式),高 29 位的值聚合為一個范圍為 0~2^29-1 的整數,表示在阻塞隊列中等待的協程個數.

2.3 加鎖Mutex.Lock() (了解即可)

在之前說過一個鎖要實現加鎖和解鎖的操作,接下來就來看看加鎖的操作

func (m *Mutex) Lock() {// Fast path: 嘗試直接通過 CAS 搶占鎖if atomic.CompareAndSwapInt32(&m.state, 0, mutexLocked) {if race.Enabled {race.Acquire(unsafe.Pointer(m))}return}// Slow path: 處理鎖競爭或鎖已被持有的情況m.lockSlow()
}

使用 原子操作 CompareAndSwapInt32 檢查鎖狀態:如果鎖的 state0(未鎖定),則將其設為 mutexLocked(1),表示鎖被當前 Goroutine 持有。

否則就進入lockslow

來看下這個lockslow

func (m *Mutex) lockSlow() {var waitStartTime int64starving := falseawoke := falseiter := 0old := m.state
? waitStartTime:標識當前 goroutine 在搶鎖過程中的等待時長,單位:ns;
? starving:標識當前是否處于饑餓模式;
? awoke:標識當前是否已有協程在等鎖;
? iter:標識當前 goroutine 參與自旋的次數;
? old:臨時存儲鎖的 state 值.for {// 進入該 if 分支,說明搶鎖失敗,處于饑餓模式,但仍滿足自旋條件if old&(mutexLocked|mutexStarving) == mutexLocked && runtime_canSpin(iter) {// 進入該 if 分支,說明當前鎖阻塞隊列有協程,但還未被喚醒,因此需要將      // mutexWoken 標識置為 1,避免再有其他協程被喚醒和自己搶鎖if !awoke && old&mutexWoken == 0 && old>>mutexWaiterShift != 0 &&atomic.CompareAndSwapInt32(&m.state, old, old|mutexWoken) {awoke = true}runtime_doSpin()iter++old = m.statecontinue}// ......}if race.Enabled {race.Acquire(unsafe.Pointer(m))}
}
  • 走進 for 循環;
  • 假如滿足三個條件:I 鎖已被占用、 II 鎖為正常模式、III 滿足自旋條件(runtime_canSpin 方法),則進入自旋后處理環節;
  • 在自旋后處理中,假如當前鎖有尚未喚醒的阻塞協程,則通過 CAS 操作將 state 的 mutexWoken 標識置為 1,將局部變量 awoke 置為 true;
  • 調用 runtime_doSpin 告知調度器 P 當前處于自旋模式;
  • 更新自旋次數 iter 和鎖狀態值 old;
  • 通過 continue 語句進入下一輪嘗試.

上面的部分可以自旋的情況,當一定次數的自旋之后,會改變狀態,調整字段,然后進入悲觀狀態,我們來看看,簡單過一遍吧,結合ai的解讀

func (m *Mutex) lockSlow() {// ......for {// ......new := old// 若非饑餓模式,嘗試直接獲取鎖if old&mutexStarving == 0 {new |= mutexLocked}// 若鎖已被持有或處于饑餓模式,增加等待者數量if old&(mutexLocked|mutexStarving) != 0 {new += 1 << mutexWaiterShift}// 若鎖已被持有或處于饑餓模式,增加等待者數量if starving && old&mutexLocked != 0 {new |= mutexStarving}// 清除喚醒標志(若當前協程已被喚醒)if awoke {.if new&mutexWoken == 0 {throw("sync: inconsistent mutex state")}new &^= mutexWoken}if atomic.CompareAndSwapInt32(&m.state, old, new) {// 成功獲取鎖(僅在非饑餓模式且鎖未被持有時可能)if old&(mutexLocked|mutexStarving) == 0 {break }// 加入等待隊列(LIFO 或 FIFO,取決于是否已等待過)queueLifo := waitStartTime != 0if waitStartTime == 0 {waitStartTime = runtime_nanotime()}// 阻塞等待信號量喚醒runtime_SemacquireMutex(&m.sema, queueLifo, 1)starving = starving || runtime_nanotime()-waitStartTime > starvationThresholdNsold = m.stateif old&mutexStarving != 0 {// 調整狀態:減少等待者數量,可能退出饑餓模式if old&(mutexLocked|mutexWoken) != 0 || old>>mutexWaiterShift == 0 {throw("sync: inconsistent mutex state")}delta := int32(mutexLocked - 1<<mutexWaiterShift)if !starving || old>>mutexWaiterShift == 1 {// 退出饑餓模式delta -= mutexStarving}atomic.AddInt32(&m.state, delta)break}awoke = trueiter = 0} else {old = m.state// CAS 失敗,重新加載狀態}}if race.Enabled {race.Acquire(unsafe.Pointer(m))}
}

2.4 Unlock (了解即可)

func (m *Mutex) Unlock() {if race.Enabled {_ = m.staterace.Release(unsafe.Pointer(m))}new := atomic.AddInt32(&m.state, -mutexLocked)if new != 0 {m.unlockSlow(new)}
}
func (m *Mutex) unlockSlow(new int32) {if (new+mutexLocked)&mutexLocked == 0 {fatal("sync: unlock of unlocked mutex")}if new&mutexStarving == 0 {old := newfor {// If there are no waiters or a goroutine has already// been woken or grabbed the lock, no need to wake anyone.// In starvation mode ownership is directly handed off from unlocking// goroutine to the next waiter. We are not part of this chain,// since we did not observe mutexStarving when we unlocked the mutex above.// So get off the way.if old>>mutexWaiterShift == 0 || old&(mutexLocked|mutexWoken|mutexStarving) != 0 {return}// Grab the right to wake someone.new = (old - 1<<mutexWaiterShift) | mutexWokenif atomic.CompareAndSwapInt32(&m.state, old, new) {runtime_Semrelease(&m.sema, false, 1)return}old = m.state}} else {// Starving mode: handoff mutex ownership to the next waiter, and yield// our time slice so that the next waiter can start to run immediately.// Note: mutexLocked is not set, the waiter will set it after wakeup.// But mutex is still considered locked if mutexStarving is set,// so new coming goroutines won't acquire it.runtime_Semrelease(&m.sema, true, 1)}
}

這里就不再多介紹了,可以下去自己看一下源碼,借助ai,其實只要知道大致的思想,在去編寫的時候,在看即可了。

三.sync.RWMutex

  • 從邏輯上,可以把 RWMutex 理解為一把讀鎖加一把寫鎖;
  • 寫鎖具有嚴格的排他性,當其被占用,其他試圖取寫鎖或者讀鎖的 goroutine 均阻塞;
  • 讀鎖具有有限的共享性,當其被占用,試圖取寫鎖的 goroutine 會阻塞,試圖取讀鎖的 goroutine 可與當前 goroutine 共享讀鎖;
  • 綜上可見,RWMutex 適用于讀多寫少的場景,最理想化的情況,當所有操作均使用讀鎖,則可實現去無化;最悲觀的情況,倘若所有操作均使用寫鎖,則 RWMutex 退化為普通的 Mutex

3.1 核心數據結構

 type RWMutex struct {w           Mutex        // held if there are pending writerswriterSem   uint32       // semaphore for writers to wait for completing readersreaderSem   uint32       // semaphore for readers to wait for completing writersreaderCount atomic.Int32 // number of pending readersreaderWait  atomic.Int32 // number of departing readers
}
  • rwmutexMaxReaders:共享讀鎖的 goroutine 數量上限,值為 2^29;
  • w:RWMutex 內置的一把普通互斥鎖 sync.Mutex;
  • writerSem:關聯寫鎖阻塞隊列的信號量;
  • readerSem:關聯讀鎖阻塞隊列的信號量;
  • readerCount:正常情況下等于介入讀鎖流程的 goroutine 數量;當 goroutine 接入寫鎖流程時,該值為實際介入讀鎖流程的 goroutine 數量減 rwmutexMaxReaders.
  • readerWait:記錄在當前 goroutine 獲取寫鎖前,還需要等待多少個 goroutine 釋放讀鎖.

源碼的走讀就不再寫了,后續在學分布式鎖的時候在完善。

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

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

相關文章

Ubuntu 服務器軟件更新,以及常用軟件安裝 —— 一步一步配置 Ubuntu Server 的 NodeJS 服務器詳細實錄 3

前言 前面&#xff0c;我們已經 安裝好了 Ubuntu 服務器系統&#xff0c;并且 配置好了 ssh 免密登錄服務器 &#xff0c;現在&#xff0c;我們要來進一步的設置服務器。 那么&#xff0c;本文&#xff0c;就是進行服務器的系統更新&#xff0c;以及常用軟件的安裝 調整 Ubu…

如何從零開始建設一個網站?

當你沒有建站的基礎和建站的知識&#xff0c;那么應該如何開展網站建設和網站管理。而今天的教程是不管你是為自己建站還是為他人建站都適合的。本教程會指導你如何進入建站&#xff0c;將建站的步驟給大家分解&#xff1a; 首先我們了解一下&#xff0c;建站需要那些步驟和流程…

網絡可靠性的定義與核心要素

網絡可靠性&#xff08;Network Reliability&#xff09;是指網絡系統在特定時間范圍內持續提供穩定、無中斷、符合預期性能的服務能力。其核心目標是確保數據能夠準確、完整、及時地傳輸&#xff0c;即使在部分故障或異常情況下仍能維持基本功能。 1. 網絡可靠性的核心指標 衡…

GpuGeek如何成為AI基礎設施市場的中堅力量

AI時代&#xff0c;算力基礎設施已成為支撐技術創新和產業升級的關鍵要素。作為國內專注服務算法工程師群體的智算平臺&#xff0c;GpuGeek通過持續創新的服務模式、精準的市場定位和系統化的生態建設&#xff0c;正快速成長為AI基礎設施領域的中堅力量。本文將深入分析GpuGeek…

【Qt】Bug:findChildren找不到控件

使用正確的父對象調用 findChildren&#xff1a;不要在布局對象上調用 findChildren&#xff0c;而應該在布局所在的窗口或控件上調用。

【Linux網絡編程】傳輸層協議TCP,UDP

目錄 一&#xff0c;UDP協議 1&#xff0c;UDP協議的格式 2&#xff0c;UDP的特點 3&#xff0c;面向數據報 4&#xff0c;UDP的緩沖區 5&#xff0c;UDP使用注意事項 6&#xff0c;基于UDP的應用層協議 二&#xff0c;對于報文的理解 三&#xff0c;TCP協議 1&…

Neo4j 數據可視化與洞察獲取:原理、技術與實踐指南

在關系密集型數據的分析領域,Neo4j 憑借其強大的圖數據模型脫穎而出。然而,將復雜的連接關系轉化為直觀見解,需要專業的數據可視化技術和分析方法。本文將深入探討 Neo4j 數據可視化的核心原理、關鍵技術、實用技巧以及結合圖數據科學庫(GDS)獲取深度洞察的最佳實踐。 Ne…

樹莓派超全系列教程文檔--(55)如何使用網絡文件系統NFS

如何使用網絡文件系統NFS 網絡文件系統 (NFS)設置基本 NFS 服務器Portmap 鎖定&#xff08;可選&#xff09; 配置 NFS 客戶端端口映射鎖定&#xff08;可選&#xff09; 配置復雜的 NFS 服務器組權限DNS&#xff08;可選&#xff0c;僅在使用 DNS 時&#xff09;NIS&#xff0…

無法運用pytorch環境、改環境路徑、隔離環境

一.未建虛擬環境時 1.創建新項目后&#xff0c;直接運行是這樣的。 2.設置中Virtualenv找不到pytorch環境&#xff1f;因為此時沒有創建新虛擬環境。 3.選擇conda環境&#xff08;全局環境&#xff09;時&#xff0c;是可以下載環境的。 運行結果如下&#xff1a; 是全局環境…

HTML5+CSS3+JS小實例:具有粘性重力的磨砂玻璃導航欄

實例:具有粘性重力的磨砂玻璃導航欄 技術棧:HTML+CSS+JS 效果: 源碼: 【HTML】 <!DOCTYPE html> <html lang="zh-CN"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width…

NodeJS全棧WEB3面試題——P8項目實戰類問題(偏全棧)

&#x1f4e6; 8.1 請描述你做過的 Web3 項目&#xff0c;具體技術棧和你負責的模塊&#xff1f; 我主導開發過一個基于 NFT 的數字紀念平臺&#xff0c;用戶可以上傳照片并生成獨特的紀念 NFT&#xff0c;結合 IPFS 和 ERC-721 實現永存上鏈。 &#x1f527; 技術棧&#xf…

3-10單元格行、列號獲取(實例:表格選與維度轉換)學習筆記

************************************************************************************************************** 點擊進入 -我要自學網-國內領先的專業視頻教程學習網站 *******************************************************************************************…

AI問答-vue3+ts+vite:http://www.abc.com:3022/m-abc-pc/#/snow 這樣的項目 在服務器怎么部署

為什么記錄有子路徑項目的部署&#xff0c;因為&#xff0c;通過子路徑可以區分項目&#xff0c;那么也就可以實現微前端架構&#xff0c;并且具有獨特優勢&#xff0c;每個項目都是絕對隔離的。 要將 Vue3 項目&#xff08;如路徑為 http://www.abc.com:3022/m-saas-pc/#/sno…

PostgreSQL-基于PgSQL17和11版本導出所有的超表建表語句

最新版本更新 https://code.jiangjiesheng.cn/article/368?fromcsdn 推薦 《高并發 & 微服務 & 性能調優實戰案例100講 源碼下載》 1. 基于pgsql 17.4 研究 查詢psql版本&#xff1a;SELECT version(); 查看已知1條建表語句和db中數據關系 SELECT create_hypert…

世事無常,比較復雜,人可以簡單一點

2025年6月5日日&#xff0c;17~28℃&#xff0c;一般 待辦&#xff1a; 宣講會 職稱材料的最后檢查 職稱材料有錯誤&#xff0c;需要修改 期末考試試題啟用 教學技能大賽PPT 遇見&#xff1a;部門宣傳泰國博士項目、碩士項目、本科項目。 感受或反思&#xff1a;東南亞博士…

B站緩存視頻數據m4s轉mp4

B站緩存視頻數據m4s轉mp4 結構分析 結構分析 在沒有改變數據存儲目錄的情況下&#xff0c;b站默認數據保存目錄為&#xff1a; Android->data->tv.danmaku.bili->download每個文件夾代表一個集合的視頻&#xff0c;比如&#xff0c;我下載的”java從入門到精通“&…

一次Oracle的非正常關閉

數據庫自己會關閉嗎&#xff1f; 從現象來說Oracle MySQL Redis等都會出現進程意外停止的情況。而這些停止都是非人為正常關閉或者暴力關閉&#xff08;abort或者kill 進程&#xff09; 一次測試環境的非關閉 一般遇到這種情況先看一下錯誤日志吧。 2025-06-01T06:26:06.35…

linux 串口調試命令 stty

linux 串口調試命令 stty 文章目錄 linux 串口調試命令 sttystty 常見命令選項&#xff1a;常用參數&#xff1a;一次性設置串口所有常見參數總結 stty&#xff08;設置終端行模式&#xff09;命令是用來配置終端設備&#xff08;包括串口設備&#xff09;的輸入和輸出行為的工…

【地址區間劃分】

地址區間劃分 1 decode_addr1.1 地址區間1.2 變式 本篇博客主要介紹對地址區間劃分的一個比較巧妙參數化的做法。 1 decode_addr 遇到一個master轉多個slave時&#xff0c;不可避免需要進行對addr總線進行分配地址區間來進行選中&#xff1b; 在這里給出一個可復用且設計思想比…

mysql復合查詢mysql子查詢

基礎表結構創建 表結構包含主外鍵約束和字符集配置&#xff0c;確保數據完整性 部門表 CREATE TABLE dept (deptno int NOT NULL COMMENT 部門編號,dname varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT 部門名稱,loc varchar(20) CHARACTE…