Go語言sync鎖與對象池的實現(xiàn)
1. 背景
在并發(fā)編程中,正確地管理共享資源是構(gòu)建高性能程序的關(guān)鍵。Go 語言標準庫中的 sync 包提供了一組基礎(chǔ)而強大的并發(fā)原語,用于實現(xiàn)安全的協(xié)程間同步與資源控制。本文將簡要介紹 sync 包中常用的類型和方法: sync 鎖 與 對象池,幫助開發(fā)者更高效地編寫并發(fā)安全的 Go 程序。
2. 鎖
go語言是出了名的高并發(fā)利器 , 但在高并發(fā)場景下 , 伴隨而來的數(shù)據(jù)安全問題是需要解決的。 加鎖就是其中的一個解決辦法。
多個線程同時訪問臨界區(qū),鎖住一些共享資源, 以防止并發(fā)訪問這些共享數(shù)據(jù)時可能導(dǎo)致的數(shù)據(jù)不一致問題。
獲取鎖的線程可以正常訪問臨界區(qū),未獲取到鎖的線程等待鎖釋放后可以嘗試獲取鎖。

sync.Locker 是 go 標準庫 sync 下定義的鎖接口:
// A Locker represents an object that can be locked and unlocked.
type Locker interface {
Lock()
Unlock()
}
任何實現(xiàn)了 Lock 和 Unlock 兩個方法的類,都可以作為一種鎖的實現(xiàn)。
Go 語言包中的 sync 包提供了兩種鎖類型:sync.Mutex 和 sync.RWMutex,前者是互斥鎖,后者是讀寫鎖。
2.1 互斥鎖 (sync.Mutex)
互斥即不可同時運行。即使用了互斥鎖的兩個代碼片段互相排斥,只有其中一個代碼片段執(zhí)行完成后,另一個才能執(zhí)行。
Go 標準庫中提供了 sync.Mutex 互斥鎖類型及其兩個方法:
Lock 加鎖
使用 Lock () 加鎖后,該線程不能再繼續(xù)對其加鎖,否則會 panic。只有在 unlock () 之后才能再次 Lock ()。異步調(diào)用 Lock (),是正當(dāng)?shù)逆i競爭,當(dāng)然不會有 panic 了。適用于讀寫不確定場景,即讀寫次數(shù)沒有明顯的區(qū)別,并且只允許只有一個讀或者寫的場景,所以該鎖也叫做全局鎖。Unlock 釋放鎖
Unlock () 用于解鎖 m,如果在使用 Unlock () 前未加鎖,就會引起一個運行錯誤。已經(jīng)鎖定的 Mutex 并不與特定的 goroutine 相關(guān)聯(lián),這樣可以利用一個 goroutine 對其加鎖,再利用其他 goroutine 對其解鎖。
2.1.1 使用方法
var lck sync.Mutex
func foo() {
lck.Lock()
defer lck.Unlock()
// ...
}
2.2 讀寫鎖 (sync.RWMutex)
讀寫鎖是分別針對讀操作和寫操作進行鎖定和解鎖操作的互斥鎖。RWMutex 提供四個方法:
func (*RWMutex) Lock // 寫鎖定 func (*RWMutex) Unlock // 寫解鎖 func (*RWMutex) RLock // 讀鎖定 func (*RWMutex) RUnlock // 讀解鎖
- 寫鎖定情況下,對讀寫鎖進行讀鎖定或者寫鎖定,都將阻塞;而且讀鎖與寫鎖之間是互斥的;
- 讀鎖定情況下,對讀寫鎖進行寫鎖定,將阻塞;加讀鎖時不會阻塞;
| 操作1 \ 操作2 | RLock()(讀鎖) | Lock()(寫鎖) | RUnlock() | Unlock() |
|---|---|---|---|---|
| RLock() | ? 并發(fā)允許 | ? 阻塞等待寫鎖釋放 | ? 無影響 | ? 無影響 |
| Lock() | ? 阻塞等待讀鎖釋放 | ? 阻塞等待寫鎖釋放 | ? 無影響 | ? 無影響 |
| RUnlock() | ? 無影響 | ? 無影響 | ? 無影響 | ? 無影響 |
| Unlock() | ? 無影響 | ? 無影響 | ? 無影響 | ? 無影響 |
讀寫鎖的存在是為了解決讀多寫少時的性能問題,讀場景較多時,讀寫鎖可有效地減少鎖阻塞的時間。
3. 對象池 (sync.Pool)
sync.Pool 的使用場景 : 保存和復(fù)用臨時對象,減少內(nèi)存分配,降低 GC 壓力。
舉例 : Gin 框架中的 context 包每次面對接口調(diào)用時都需要創(chuàng)建 ,貫穿整個調(diào)用鏈路。底層就使用對象池進行優(yōu)化。
sync.Pool 是可伸縮的,同時也是并發(fā)安全的,其大小僅受限于內(nèi)存的大小。sync.Pool 用于存儲那些被分配了但是沒有被使用,而未來可能會使用的值。
3.1 使用方法
只需要實現(xiàn) New 函數(shù)即可。對象池中沒有對象時,將會調(diào)用 New 函數(shù)創(chuàng)建。
初始化 :
var studentPool = sync.Pool{
New: func() interface{} {
return new(Student)
},
}
關(guān)鍵操作 :
// Put adds x to the pool. func (p *Pool) Put(x any); // Get selects an arbitrary item from the [Pool], removes it from the // Pool, and returns it to the caller. // Get may choose to ignore the pool and treat it as empty. // Callers should not assume any relation between values passed to [Pool.Put] and // the values returned by Get. // // If Get would otherwise return nil and p.New is non-nil, Get returns // the result of calling p.New. func (p *Pool) Get() any;
舉例 :
stu := studentPool.Get().(*Student) json.Unmarshal(buf, stu) studentPool.Put(stu)
- Get() 用于從對象池中獲取對象,因為返回值是 interface{},因此需要類型轉(zhuǎn)換。
- Put() 則是在對象使用完畢后,返回對象池。
3.2 底層解析
3.2.1 sync.Pool 數(shù)據(jù)結(jié)構(gòu)
type Pool struct {
noCopy noCopy
local unsafe.Pointer // local fixed-size per-P pool, actual type is [P]poolLocal
localSize uintptr // size of the local array
victim unsafe.Pointer // local from previous cycle
victimSize uintptr // size of victims array
// New optionally specifies a function to generate
// a value when Get would otherwise return nil.
// It may not be changed concurrently with calls to Get.
New func()
}
• noCopy 防拷貝標志;
• local 類型為 [P]poolLocal 的數(shù)組,數(shù)組容量 P 為 goroutine 處理器 P 的個數(shù);
• victim 為經(jīng)過一輪 GC 回收,暫存的上一輪 local;類型于二級緩存 , 隨時可能被GC 回收
• New 為用戶指定的工廠函數(shù),當(dāng) Pool 內(nèi)存量元素不足時,會調(diào)用該函數(shù)構(gòu)造新的元素.
[P]poolLocal 數(shù)組
暫時存儲對象的對象池 , 每個 poolLocal 邏輯處理器分為 private 和 sharedList 兩部分緩存數(shù)據(jù)
type poolLocal struct {
poolLocalInternal
}
// Local per-P Pool appendix.
type poolLocalInternal struct {
private any // Can be used only by the respective P.
shared poolChain // Local P can pushHead/popHead; any P can popTail.
}
- poolLocal 為 Pool 中對應(yīng)于某個 P 的緩存數(shù)據(jù);
- poolLocalInternal.private:對應(yīng)于某個 P 的私有元素,操作時無需加鎖;
- poolLocalInternal.shared: 某個 P 下的共享元素鏈表,由于各 P 都有可能訪問,因此需要加鎖.

3.2.2 sync.Pool 的核心方法
3.2.2.1 Pool.Get
Get流程

func (p *Pool) Get() any {
l, pid := p.pin()
x := l.private
l.private = nil
if x == nil {
x, _ = l.shared.popHead()
if x == nil {
x = p.getSlow(pid)
}
}
runtime_procUnpin()
if x == nil && p.New != nil {
x = p.New()
}
return x
}
- 調(diào)用 Pool.pin 方法,綁定當(dāng)前 goroutine 與 P,并且取得該 P 對應(yīng)的緩存數(shù)據(jù);
- 嘗試獲取 P 緩存數(shù)據(jù)的私有元素 private;
- 倘若前一步失敗,則嘗試取 P 緩存數(shù)據(jù)中共享元素鏈表的頭元素;
- 倘若前一步失敗,則走入 Pool.getSlow 方法,嘗試取其他 P 緩存數(shù)據(jù)中共享元素鏈表的尾元素;
- 同樣在 Pool.getSlow 方法中,倘若前一步失敗,則嘗試從上輪 gc 前緩存中取元素(victim);
- 調(diào)用 native 方法解綁 當(dāng)前 goroutine 與 P
- 倘若(2)-(5)步均取值失敗,調(diào)用用戶的工廠方法,進行元素構(gòu)造并返回.
3.2.2.1 Pool.Put
Put流程

/ Put adds x to the pool.
func (p *Pool) Put(x any) {
if x == nil {
return
}
l, _ := p.pin()
if l.private == nil {
l.private = x
} else {
l.shared.pushHead(x)
}
runtime_procUnpin()
}
- 判斷存入元素 x 非空;
- 調(diào)用 Pool.pin 綁定當(dāng)前 goroutine 與 P,并獲取 P 的緩存數(shù)據(jù);
- 倘若 P 緩存數(shù)據(jù)中的私有元素為空,則將 x 置為其私有元素;
- 倘若未走入(3)分支,則將 x 添加到 P 緩存數(shù)據(jù)共享鏈表的末尾;
- 解綁當(dāng)前 goroutine 與 P.
3.2.2 對象池的回收
存入 pool 的對象會不定期被 go 運行時回收,因此 pool 沒有容量概念,即便大量存入元素,也不會發(fā)生內(nèi)存泄露.
具體回收時機是在 gc 時執(zhí)行的:
- 每個 Pool 首次執(zhí)行 Get 方法時,會在內(nèi)部首次調(diào)用 pinSlow 方法內(nèi)將該 pool 添加到遷居的 allPools 數(shù)組中;
- 每次 gc 時,會將上一輪的 oldPools 清空,并將本輪 allPools 的元素賦給 oldPools,allPools 置空;
- 新置入 oldPools 的元素統(tǒng)一將 local 轉(zhuǎn)移到 victim,并且將 local 置為空.
綜上可以得見,最多兩輪 gc,pool 內(nèi)的對象資源將會全被回收.
到此這篇關(guān)于Go語言sync鎖與對象池的實現(xiàn)的文章就介紹到這了,更多相關(guān)Go語言sync鎖與對象池內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
解決Goland 同一個package中函數(shù)互相調(diào)用的問題
這篇文章主要介紹了解決Goland 同一個package中函數(shù)互相調(diào)用的問題,具有很好的參考價值,希望對大家有所幫助。一起跟隨小編過來看看吧2021-05-05
一文理解Goland協(xié)程調(diào)度器scheduler的實現(xiàn)
本文主要介紹了Goland協(xié)程調(diào)度器scheduler的實現(xiàn),文中通過示例代碼介紹的非常詳細,對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2022-06-06

