golang原始碼分析:sync.Pool 如何從讀寫加鎖到無鎖
我們知道,早期的sync.Pool,底層是通過互斥鎖實現對共享佇列的併發訪問的,這裡會存在的問題是,儘管是分段鎖,高併發場景下頻繁進行G的wait和runable狀態排程其實開銷也不算小
通過互斥鎖保證併發安全的sync.Pool資料結構
type Pool struct {
noCopy noCopy
local unsafe.Pointer // local,固定大小per-P池, 實際型別為 [P]poolLocal
localSize uintptr // local array 的大小
// New 方法在 Get 失敗的情況下,選擇性的建立一個值, 否則返回nil
New func() interface{}
}
type poolLocal struct {
poolLocalInternal
// 將 poolLocal 補齊至兩個快取行的倍數,防止 false sharing,
// 每個快取行具有 64 bytes,即 512 bit
// 目前我們的處理器一般擁有 32 * 1024 / 64 = 512 條快取行
pad [128 - unsafe.Sizeof(poolLocalInternal{})%128]byte
}
// Local per-P Pool appendix.
type poolLocalInternal struct {
private interface{} // 只能被區域性排程器P使用
shared []interface{} // 所有P共享
Mutex // 讀寫shared前要加鎖
}
通過雙向連結串列實現的無鎖sync.Pool資料結構
local和victim都指向poolLocal陣列,它們的區別是: pool裡面的兩個poolLocal陣列,每次經歷STW後victim置零,將local賦值給victim,local置零。
這裡的allPools和oldPools都儲存的Pool集合。allPools儲存的是未經歷STW的Pool,使用的是pool的local欄位;allPools經歷STW之後將local變成victim,allPools變成oldPools。這兩個變數不參與存取過程,僅在STW的時候使用。
poolCleanup
func init() {
runtime_registerPoolCleanup(poolCleanup)
}
sync.Pool在init()中向runtime註冊了一個cleanup方法,它在STW1階段被呼叫的
也就是說,pool中快取的物件,最多生存2次GC就會完全回收
資料結構
我們看一下這裡put和get的方法
func (p *Pool) Put(x interface{}) {
if x == nil {
return
}
// . . .
l, _ := p.pin() // 拿到g所屬p的poolLocal, 並鎖定
// 這裡取消了 mutex 呼叫.
if l.private == nil {
l.private = x
x = nil
}
if x != nil {
// 歸還物件
l.shared.pushHead(x)
}
runtime_procUnpin()
if race.Enabled {
race.Enable()
}
}
func (p *Pool) Get() interface{} {
if race.Enabled {
race.Disable()
}
l, pid := p.pin()
x := l.private
l.private = nil
if x == nil {
x, _ = l.shared.popHead() // 從本地的shared拿物件
if x == nil {
x = p.getSlow(pid) // 嘗試偷物件
}
}
runtime_procUnpin()
if x == nil && p.New != nil {
x = p.New() // 沒偷到物件,新建一個
}
return x
}
func (p *Pool) getSlow(pid int) interface{} {
size := atomic.LoadUintptr(&p.localSize) // load-acquire
locals := p.local // load-consume
// Try to steal one element from other procs.
for i := 0; i < int(size); i++ {
l := indexLocal(locals, (pid+i+1)%int(size))
if x, _ := l.shared.popTail(); x != nil {
return x
}
}
. . .
}
// 從連結串列尾取物件,這裡是一個併發操作
func (c *poolChain) popTail() (interface{}, bool) {
d := loadPoolChainElt(&c.tail)
if d == nil {
return nil, false
}
for {
d2 := loadPoolChainElt(&d.next)
if val, ok := d.popTail(); ok {
return val, ok
}
if d2 == nil {
return nil, false
}
// 使用atomic cas來替換tail
if atomic.CompareAndSwapPointer((*unsafe.Pointer)(unsafe.Pointer(&c.tail)), unsafe.Pointer(d), unsafe.Pointer(d2)) {
storePoolChainElt(&d2.prev, nil)
}
d = d2
}
}
為什麼這裡用cas替換了Mutex
大家知道cas的機制是自旋重試,它適用的場景是鎖粒度小、釋放快的場景,這樣cas快速重試幾次就能成功
而互斥鎖會導致協程gopark\進入waitqueue,釋放鎖後又被goready叫醒再被排程
對於pool的場景下,本身是鎖粒度小、釋放快的場景,相比互斥鎖,cas大概率原地重試幾下就能拿到鎖,相比切換協程狀態重新排程,效能開銷就會低一些。
相關文章
- golang RWMutex讀寫互斥鎖原始碼分析GolangMutex原始碼
- Golang 讀寫鎖RWMutex 互斥鎖Mutex 原始碼詳解GolangMutex原始碼
- 故障分析 | 從 Insert 併發死鎖分析 Insert 加鎖原始碼邏輯原始碼
- Java 讀寫鎖 ReentrantReadWriteLock 原始碼分析Java原始碼
- 原始碼分析:升級版的讀寫鎖 StampedLock原始碼
- 原始碼分析:ReentrantReadWriteLock之讀寫鎖原始碼
- 從自旋鎖、睡眠鎖、讀寫鎖到 Linux RCU 機制講解Linux
- 從ReentrantLock加鎖解鎖角度分析AQSReentrantLockAQS
- Redisson 分散式鎖原始碼 01:可重入鎖加鎖Redis分散式原始碼
- mysql加鎖讀MySql
- ReentrantReadWriterLock原始碼(state設計、讀寫鎖、共享鎖、獨佔鎖及鎖降級)原始碼
- ZooKeeper 分散式鎖 Curator 原始碼 03:可重入鎖併發加鎖分散式原始碼
- For Update 加鎖分析
- sync.pool 原始碼閱讀原始碼
- ZooKeeper 分散式鎖 Curator 原始碼 02:可重入鎖重複加鎖和鎖釋放分散式原始碼
- Golang 基礎值速學之二十一(讀寫鎖互斥鎖)Golang
- 併發程式設計之——寫鎖原始碼分析程式設計原始碼
- Java併發指南10:Java 讀寫鎖 ReentrantReadWriteLock 原始碼分析Java原始碼
- 可重入鎖原始碼分析原始碼
- MySql 中有 select … for update 來加讀鎖,那麼對應地在 DocumentDB中 如何加讀鎖MySql
- 鎖機制到加鎖的必要性
- Java使用讀寫鎖替代同步鎖Java
- InnoDB 事務加鎖分析
- MySQL加鎖處理分析MySql
- MySQL 加鎖處理分析MySql
- InnoDB事務鎖之行鎖-insert加鎖-隱式鎖加鎖原理
- 淺談Java中的鎖:Synchronized、重入鎖、讀寫鎖Javasynchronized
- MySQL MyISAM引擎的讀鎖與寫鎖MySql
- MySQL死鎖系列-常見加鎖場景分析MySql
- Go 互斥鎖 Mutex 原始碼分析(二)GoMutex原始碼
- Java併發-顯式鎖篇【可重入鎖+讀寫鎖】Java
- MySQL鎖問題分析-全域性讀鎖MySql
- 併發程式設計 —— 原始碼分析公平鎖和非公平鎖程式設計原始碼
- Java讀寫鎖ReadWriteLockJava
- Go語言之讀寫鎖Go
- Java中的讀/寫鎖Java
- 併發程式設計之——讀鎖原始碼分析(解釋關於鎖降級的爭議)程式設計原始碼
- MySQL如何加鎖控制併發MySql