鎖 原子操作和golang mutex原始碼詳解

2021-09-24 12:49:24 字數 3413 閱讀 4525

對於某一塊**段,多個執行緒或者協程同時執行會產生一些不符合預期的結果,就需要使用訊號量保護這一段**區,只能由乙個執行緒來占用和執行這段**.這相當於是乙個大型的原子操作,由軟體層面來實現.下面是一段結果不符合預期的**段:

var count =0

func main() 

wg.add(2)

go add(&wg)

go add(&wg)

wg.wait()

fmt.println(count)

}func add(wg *sync.waitgroup)

wg.done()

}結果大多數情況下,都不是20000,這和預期結果是不一樣的.我們來分析一下為什麼會和預期不一樣:

count++操作編譯成彙編指令操作時(cpu只認這種指令),會分成這幾步,

1.cpu從記憶體中獲取count值

2.執行count加1操作

3.將count值寫到快取(或者記憶體中)

每一步都是原子操作(不可中斷操作並且在這個時間點會獨佔某個資源),這三步合在一塊就不是原子操作了.可能會出現兩個執行緒(在不同的cpu上)都執行了讀取count操作,其中乙個執行緒執行了加一操作並將count值寫到記憶體中更新,另外乙個執行緒對這一操作無感知,並繼續操作自己的舊值,結果就和預期不一致了.

單處理器不會出現這種情況,只有多處理器才會出現和預期不符的結果.

這個時候就需要加鎖(讓count++操作變成軟體層面的原子操作)或者使用atomic的add操作(硬體層面的原子操作),同一時間點只讓乙個cpu不可中斷的執行這些指令.

概念就是對於某個指令或者某個**段或者某個資源,在同一時間內,有且僅有乙個cpu可以執行,並且是不可中斷的,即一旦開始執行就執行到**段末尾.

硬體層面上,有兩種實現方案,目前cpu架構多採用第二種方案.

1.對匯流排加鎖.在x86 平台上,cpu提供了在指令執行期間對匯流排加鎖的手段。對匯流排加鎖以後,別的cpu就不能通過匯流排訪問記憶體資料了.顯而易見,這個加鎖指令在效能上有些不盡人意,如果有其他cpu想訪問別的資源,也是訪問不了的,我認為鎖的粒度實在太大,嚴重影響cpu執行效率.

2.使用快取一致性協議,對快取行加鎖。當cpu寫資料時,如果發現操作的變數是共享變數,即在其他cpu中也存在該變數的副本,會發出訊號通知其他cpu將該變數的快取行置為無效狀態,因此當其他cpu需要讀取這個變數時,發現自己快取中快取該變數的快取行是無效的,那麼它就會從記憶體重新讀取。

軟體層面,使用硬體層面的原子操作機制,獲取臨界區**段的操作權,然後執行臨界區**段.大多數程式語言鎖的實現都是借助於硬體層面實現的加鎖機制,使用作業系統(借助硬體加鎖)提供的一些基本原子操作(cas,add操作等等)完成對鎖的實現,golang鎖的實現也不例外.

對鎖和原子操作有了基本認識以後,再閱讀golang提供的鎖原始碼就不費勁了.根據原始碼進行逐步解釋,有些細節本人也沒細看,只能說個大概意思.golang版本1.10.3

先講一下mutex鎖的兩種模式,正常模式和飢餓模式

正常模式,協程在先進先出的佇列裡進行排隊,前乙個協程執行結束解鎖時,會喚醒等待佇列中的乙個協程,喚醒的協程和剛來的協程(還沒進入佇列)競爭鎖的所有權.剛進來的協程當前正在持有著cpu,資源也不需要重新排程,剛喚醒的協程大概率競爭不過新來的協程.當喚醒的協程沉睡超過1ms,會將鎖置為飢餓模式.

飢餓模式,解鎖的協程將鎖的所有權移交給等待佇列中的隊首協程,新到來的協程不會嘗試去獲取鎖的控制權(即時當前是可獲取狀態),也不會嘗試去自旋,會直接加入到隊尾等待被喚醒.

飢餓模式的解除,當前獲取mutex所有權的協程是阻塞佇列的最後乙個協程或者該協程等待時間小於1ms,則將飢餓模式轉換成正常模式.

正常模式和飢餓模式優劣,正常模式效能會更高一些,它傾向於新來的協程獲取到mutex的所有權,減少了資源切換過程(喚醒老協程需要重新排程資源到cpu中).飢餓模式是防止某個協程等待時間過久,導致預期之外的問題.

// mutex結構體

type mutex struct

const (

mutexlocked = 1 << iota // 上鎖標誌位

mutexwoken //協程有喚醒狀態

mutexstarving //mutex是否處於飢餓模式

mutexwaitershift = iota //當前等待mutex的協程數量偏移

)// lock 獲取鎖的控制權,對部分和本文內容關聯不高的**做了刪減

func (m *mutex) lock()

var waitstarttime int64 //當前協程等待了多長時間

starving := false //該協程是否要進入飢餓模式標識

awoke := false //當前協程是否處於喚醒狀態標識

iter := 0 //自旋次數標識

old := m.state

for

runtime_dospin()//自旋

iter++

old = m.state//更新到最新狀態

continue

}new := old

// 新來的協程在飢餓模式下是不能直接獲取到mutex所有權的

if old&mutexstarving == 0

// 當mutex已被占有或者mutex處於飢餓模式,排隊者數量+1

if old&(mutexlocked|mutexstarving) != 0

// 當前協程將mutex切換成飢餓模式,如果當前處於解鎖狀態,則不需要切換成飢餓模式,切換起來沒有意義

if starving && old&mutexlocked != 0

if awoke

new &^= mutexwoken

}//cas操作,當前協程嘗試去獲取鎖

if atomic.compareandswapint32(&m.state, old, new)

//判斷之前是否排隊等待過mutex

queuelifo := waitstarttime != 0

if waitstarttime == 0

runtime_semacquiremutex(&m.sema, queuelifo)

starving = starving || runtime_nanotime()-waitstarttime > starvationthresholdns//等待超過1ms,則標記當前已經飢餓,需要設定mutex為飢餓模式

old = m.state

if old&mutexstarving != 0

delta := int32(mutexlocked - 1<>mutexwaitershift == 1

atomic.addint32(&m.state, delta)

break

}awoke = true

iter = 0//自旋次數置為零,進入新的迴圈,去嘗試獲取mutex的所有權

} else }}

原子操作和鎖

原子操作 在多程序 執行緒 的作業系統中不能被其它程序 執行緒 打斷的操作就叫原子操作,檔案的原子操作是指操作檔案時的不能被打斷的操作。原子操作是不可分割的,在執行過程中不會被任何其它任務或事件中斷。linux核心提供了一系列函式來實現核心中的原子操作,這些函式又分為兩類,分別針對位和整型變數進行原...

原子操作 普通鎖 讀寫鎖

一 原子操作cas compare and swap 原子操作分三步 讀取addr的值,和old進行比較,如果相等,則將new賦值給 addr,他能保證這三步一起執行完成,叫原子操作也就是說它不能再分了,當有乙個cpu在訪問這塊內容addr時,其他cpu就不能訪問 text compareandsw...

cuda 原子鎖 多執行緒操作 通用原子操作

在專案中,空間中有200w 的點,需要對映到乙個grid map的600 600的網格中,落入到同乙個格仔的點需要進行一些計算獲得乙個值。對於格仔與格仔之間是並行的,但格仔之中的點需要設計為序列。所以在計算某個格仔中的點時,需要將格仔的值保護起來,只允許乙個執行緒 點 計算並改變。這裡就用到了cud...