1、Mutex 幾種狀態
mutexLocked — 表示互斥鎖的鎖定狀態;
mutexWoken — 表示從正常模式被從喚醒;
mutexStarving — 當前的互斥鎖進入飢餓狀態;
waitersCount — 當前互斥鎖上等待的 Goroutine 個數;
2、Mutex 正常模式和飢餓模式
正常模式(非公平鎖)
正常模式下,所有等待鎖的 goroutine 按照 FIFO(先進先出)順序等待。喚醒 的 goroutine 不會直接擁有鎖,而是會和新請求 goroutine 競爭鎖。新請求的 goroutine 更容易搶佔:因為它正在 CPU 上執行,所以剛剛喚醒的 goroutine有很大可能在鎖競爭中失敗。在這種情況下,這個被喚醒的 goroutine 會加入 到等待佇列的前面。
飢餓模式(公平鎖)
為瞭解決了等待 goroutine 佇列的長尾問題 飢餓模式下,直接由 unlock 把鎖交給等待佇列中排在第一位的 goroutine (隊 頭),同時,飢餓模式下,新進來的 goroutine 不會參與搶鎖也不會進入自旋狀 態,會直接進入等待佇列的尾部。這樣很好的解決了老的 goroutine 一直搶不 到鎖的場景。
飢餓模式的觸發條件:當一個 goroutine 等待鎖時間超過 1 毫秒時,或者當前 佇列只剩下一個 goroutine 的時候,Mutex 切換到飢餓模式。
總結
對於兩種模式,正常模式下的效能是最好的,goroutine 可以連續多次獲取 鎖,飢餓模式解決了取鎖公平的問題,但是效能會下降,這其實是效能和公平 的一個平衡模式。
3、Mutex 允許自旋的條件
鎖已被佔用,並且鎖不處於飢餓模式。
積累的自旋次數小於最大自旋次數(active_spin=4)。
CPU 核數大於 1。
有空閒的 P。
當前 Goroutine 所掛載的 P 下,本地待執行佇列為空。
4、RWMutex 實現
透過記錄 readerCount 讀鎖的數量來進行控制,當有一個寫鎖的時候,會將讀 鎖數量設定為負數 1<<30。目的是讓新進入的讀鎖等待之前的寫鎖釋放通知讀鎖。同樣的當有寫鎖進行搶佔時,也會等待之前的讀鎖都釋放完畢,才會開始進行後續的操作。 而等寫鎖釋放完之後,會將值重新加上 1<<30, 並通知剛才 新進入的讀鎖(rw.readerSem),兩者互相限制。
5、RWMutex 注意事項
RWMutex 是單寫多讀鎖,該鎖可以加多個讀鎖或者一個寫鎖
讀鎖佔用的情況下會阻止寫,不會阻止讀,多個 Goroutine 可以同時獲取讀鎖
寫鎖會阻止其他 Goroutine(無論讀和寫)進來,整個鎖由該 Goroutine 獨佔
適用於讀多寫少的場景
RWMutex 型別變數的零值是一個未鎖定狀態的互斥鎖
RWMutex 在首次被使用之後就不能再被複製
RWMutex 的讀鎖或寫鎖在未鎖定狀態,解鎖操作都會引發 panic
RWMutex 的一個寫鎖去鎖定臨界區的共享資源,如果臨界區的共享資源已 被(讀鎖或寫鎖)鎖定,這個寫鎖操作的 goroutine 將被阻塞直到解鎖
RWMutex 的讀鎖不要用於遞迴呼叫,比較容易產生死鎖
RWMutex 的鎖定狀態與特定的 goroutine 沒有關聯。一個 goroutine 可 以 RLock(Lock),另一個 goroutine 可以 RUnlock(Unlock)
寫鎖被解鎖後,所有因操作鎖定讀鎖而被阻塞的 goroutine 會被喚醒,並 都可以成功鎖定讀鎖
讀鎖被解鎖後,在沒有被其他讀鎖鎖定的前提下,所有因操作鎖定寫鎖而 被阻塞的 Goroutine,其中等待時間最長的一個 Goroutine 會被喚醒
6、Cond 是什麼
Cond 實現了一種條件變數,可以使用在多個 Reader 等待共享資源 ready 的場 景(如果只有一讀一寫,一個鎖或者 channel 就搞定了)每個 Cond 都會關聯一個 Lock(*sync.Mutex or *sync.RWMutex),當修改條 件或者呼叫 Wait 方法時,必須加鎖,保護 condition。
以下摘自ChatGPT:
在 Go 語言中,Cond(條件變數)是一種用於實現多個 goroutine 之間等待和通知的同步機制。Cond 可以用於解決多個 goroutine 之間協作的問題,例如生產者和消費者問題、讀寫鎖問題等。
Cond 通常與 Mutex 一起使用,它可以讓等待鎖的 goroutine 臨時釋放鎖,等待某個條件變數滿足之後再重新獲取鎖。
Cond 包含三個主要的方法:
Wait():等待條件變數滿足,同時釋放鎖,直到另外一個 goroutine 呼叫 Signal() 或 Broadcast() 來通知條件變數已經滿足,該方法才會返回,並重新獲取鎖。
Signal():通知等待條件變數的一個 goroutine,條件變數已經滿足。
Broadcast():通知所有等待條件變數的 goroutine,條件變數已經滿足。
當一個 goroutine 呼叫 Wait() 方法時,它會將當前的 goroutine 新增到條件變數的等待佇列中,並且臨時釋放鎖。當另外一個 goroutine 呼叫 Signal() 或 Broadcast() 方法時,它會通知條件變數中等待佇列中的 goroutine,條件變數已經滿足,這些 goroutine 會被喚醒並重新獲取鎖,然後繼續執行。
透過使用 Cond,多個 goroutine 可以協作地等待和通知條件變數的狀態變化,從而實現更加高效和靈活的併發程式設計。
例子:
一個簡單的例子就是使用 Cond 實現生產者-消費者模式。
假設有一個佇列,多個生產者可以往佇列中新增資料,多個消費者可以從佇列中取出資料。由於多個 goroutine 同時對佇列進行操作,需要使用 Mutex 保證佇列的執行緒安全。同時,為了避免消費者不斷地進行無用的輪詢,可以使用 Cond 來實現消費者等待新的資料新增到佇列中的功能。
下面是使用 Cond 實現生產者-消費者模式的程式碼示例:
package main
import (
"fmt"
"sync"
)
type Queue struct {
data []int
mutex sync.Mutex
cond *sync.Cond
}
func NewQueue() *Queue {
q := &Queue{
data: make([]int, 0),
}
q.cond = sync.NewCond(&q.mutex)
return q
}
func (q *Queue) Put(v int) {
q.mutex.Lock()
defer q.mutex.Unlock()
q.data = append(q.data, v)
q.cond.Signal() // 通知消費者
}
func (q *Queue) Get() int {
q.mutex.Lock()
defer q.mutex.Unlock()
for len(q.data) == 0 {
q.cond.Wait() // 等待新的資料新增到佇列中
}
v := q.data[0]
q.data = q.data[1:]
return v
}
func main() {
q := NewQueue()
// 啟動多個生產者
for i := 0; i < 3; i++ {
go func(i int) {
for j := 0; j < 5; j++ {
v := i * 10 + j
q.Put(v)
fmt.Printf("producer %d put %d\n", i, v)
}
}(i)
}
// 啟動多個消費者
for i := 0; i < 2; i++ {
go func(i int) {
for j := 0; j < 7; j++ {
v := q.Get()
fmt.Printf("consumer %d get %d\n", i, v)
}
}(i)
}
// 等待所有生產者和消費者結束
var wg sync.WaitGroup
wg.Add(5 + 7)
wg.Wait()
}
在上面的程式碼中,生產者使用 Put() 方法往佇列中新增資料,消費者使用 Get() 方法從佇列中取出資料。當佇列中沒有資料時,消費者呼叫 Wait() 方法等待新的資料新增到佇列中,當生產者往佇列中新增資料時,會呼叫 Signal() 方法通知等待的消費者。
透過使用 Mutex 和 Cond,可以保證多個生產者和消費者能夠安全地協作地操作佇列,從而實現了生產者-消費者模式。
7、Broadcast 和 Signal 區別
func (c *Cond) Broadcast()
Broadcast 會喚醒所有等待 c 的 goroutine。 呼叫 Broadcast 的時候,可以加鎖,也可以不加鎖。
func (c *Cond) Signal()
Signal 只喚醒 1 個等待 c 的 goroutine。 呼叫 Signal 的時候,可以加鎖,也可以不加鎖。
8、Cond 中 Wait 使用
func (c *Cond) Wait()
Wait()
會自動釋放 c.L 鎖
,並掛起呼叫者的 goroutine。之後恢復執行,
Wait()
會在返回時對 c.L
加鎖。
除非被 Signal 或者 Broadcast 喚醒,否則 Wait()
不會返回。
由於 Wait()
第一次恢復時,C.L
並沒有加鎖,所以當 Wait 返回時,呼叫者通常 並不能假設條件為真。如下程式碼:
取而代之的是, 呼叫者應該在迴圈中呼叫 Wait。(簡單來說,只要想使用 condition,就必須加鎖。)
c.L.Lock()
for !condition() {
c.Wait()
}
... make use of condition ...
c.L.Unlock()
9、WaitGroup 用法
一個 WaitGroup 物件可以等待一組協程結束。使用方法是:
main 協程透過呼叫 wg.Add(delta int) 設定 worker 協程的個數,然後創 建 worker 協程;
worker 協程執行結束以後,都要呼叫 wg.Done();
main 協程呼叫 wg.Wait() 且被 block,直到所有 worker 協程全部執行結束 後返回。
10、WaitGroup 實現原理
WaitGroup 主要維護了 2 個計數器,一個是請求計數器 v,一個是等待計數 器 w,二者組成一個 64bit 的值,請求計數器佔高 32bit,等待計數器佔低 32bit。
每次 Add 執行,請求計數器 v 加 1,Done 方法執行,等待計數器減 1,v 為 0 時透過訊號量喚醒 Wait()。
11、什麼是 sync.Once
Once 可以用來執行且僅僅執行一次動作,常常用於單例物件的初始化場景
Once 常常用來初始化單例資源,或者併發訪問只需初始化一次的共享資 源,或者在測試的時候初始化一次測試資源。
sync.Once 只暴露了一個方法 Do,你可以多次呼叫 Do 方法,但是隻有第 一次呼叫 Do 方法時 f 引數才會執行,這裡的 f 是一個無引數無返回值 的函式。
以下摘自ChatGPT:
sync.Once 是 Go 語言中的一個同步原語,用於保證一個函式只被執行一次。
Once 型別有一個 Do 方法,該方法接受一個函式作為引數,只有在第一次呼叫 Do 方法時,才會執行該函式。後續呼叫 Do 方法會直接返回,不會再次執行傳入的函式。
使用 sync.Once 可以很方便地實現單例模式,例如在需要使用單例的場景下,我們可以將單例的初始化操作放在 sync.Once 的 Do 方法中,保證只有在第一次使用單例時進行初始化操作,後續使用直接返回已經初始化過的單例例項。
下面是一個使用 sync.Once 實現單例模式的簡單示例程式碼:
package main
import (
"fmt"
"sync"
)
type Singleton struct {
name string
}
var (
instance *Singleton
once sync.Once
)
func GetInstance() *Singleton {
once.Do(func() {
instance = &Singleton{name: "singleton instance"}
})
return instance
}
func main() {
// 第一次呼叫 GetInstance 會進行初始化操作
singleton1 := GetInstance()
fmt.Println(singleton1.name)
// 再次呼叫 GetInstance 不會進行初始化操作
singleton2 := GetInstance()
fmt.Println(singleton2.name)
// 兩次呼叫返回的是同一個例項
fmt.Println(singleton1 == singleton2)
}
在上面的示例中,GetInstance() 函式使用 sync.Once 來保證 Singleton 型別的例項只被建立一次,後續的呼叫都直接返回已經建立的例項。這樣就可以方便地實現單例模式,避免了多次建立相同的物件,節省了系統資源。
12、什麼操作叫做原子操作
原子操作即是進行過程中不能被中斷的操作,針對某個值的原子操作在被進行 的過程中,CPU 絕不會再去進行其他的針對該值的操作。為了實現這樣的嚴謹 性,原子操作僅會由一個獨立的 CPU 指令代表和完成。原子操作是無鎖的,常 常直接透過 CPU 指令直接實現。 事實上,其它同步技術的實現常常依賴於原 子操作。
13、原子操作和鎖的區別
原子操作由底層硬體支援,而鎖則由作業系統的排程器實現。
鎖應當用來保護一段邏輯,對於一個變數更新的保護。
原子操作通常執行上會更有效率,並且更能利用計算機多核的優勢,如果要更新的是一個複合物件,則應當使用 atomic.Value
封裝好的實現。
14、什麼是 CAS
CAS 的全稱為Compare And Swap
,直譯就是比較交換。是一條 CPU 的原子指 令,其作用是讓 CPU 先進行比較兩個值是否相等,然後原子地更新某個位置的 值,其實現方式是給予硬體平臺的彙編指令,在 intel 的 CPU 中,使用的 cmpxchg 指令,就是說 CAS 是靠硬體實現的,從而在硬體層面提升效率。
簡述過程是這樣:
假設包含 3 個引數記憶體位置(V)、預期原值(A)和新值(B)。V
表示要更新變數的 值,E
表示預期值,N
表示新值。僅當 V
值等於E
值時,才會將V
的值設為N
, 如果 V
值和E
值不同,則說明已經有其他執行緒在做更新,則當前執行緒什麼都不 做,最後 CAS
返回當前 V
的真實值。CAS 操作時抱著樂觀的態度進行的,它總 是認為自己可以成功完成操作。基於這樣的原理,CAS 操作即使沒有鎖,也可 以發現其他執行緒對於當前執行緒的干擾。
15、sync.Pool 有什麼用
對於很多需要重複分配、回收記憶體的地方,sync.Pool
是一個很好的選擇。頻 繁地分配、回收記憶體會給 GC 帶來一定的負擔,嚴重的時候會引起 CPU 的毛 刺。而 sync.Pool
可以將暫時將不用的物件快取起來,待下次需要的時候直 接使用,不用再次經過記憶體分配,複用物件的記憶體,減輕 GC 的壓力,提升系 統的效能。
本作品採用《CC 協議》,轉載必須註明作者和本文連結