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