深度剖析golang中的sync包
簡(jiǎn)介
golang的sync包提供了一些并發(fā)控制的工具,在應(yīng)用程序開(kāi)發(fā)過(guò)程中是非常有用的,下面詳細(xì)介紹下這些工具的原理和使用- 在介紹工具之前,先講解下內(nèi)存模型中的
happens-before關(guān)系
// 示例:Happens-Before關(guān)系
var a string
var done bool
func setup() {
a = "hello, world" // 寫操作A
done = true // 寫操作B
}
func main() {
go setup()
for !done { // 讀操作C
// 忙等待
}
print(a) // 讀操作D
}關(guān)鍵點(diǎn):
- 如果B happens-before C,那么A happens-before D
- sync包的作用就是建立這種happens-before關(guān)系
Mutex
golang提供的鎖,常用于保護(hù)共享資源的訪問(wèn)安全,使用如下
func main() {
var mu sync.Mutex
mu.Lock()
defer mu.Unlock()
mu.TryLock()
}
mu.Lock():加鎖mu.Unlock():解鎖mu.TryLock():嘗試加鎖,如果成功會(huì)返回true
鎖結(jié)構(gòu)如下
type Mutex struct {
state int32 // 鎖狀態(tài):包含多個(gè)標(biāo)志位
sema uint32 // 信號(hào)量:用于阻塞goroutine
}
- 加鎖的時(shí)候,會(huì)先使用
cas原子操作獲取鎖(CPU實(shí)現(xiàn)),如果拿不到說(shuō)明已經(jīng)被其他goroutine占有,通過(guò)自旋阻塞當(dāng)前goroutine(這過(guò)程會(huì)通過(guò)PAUSE指令減少CPU功耗)
func (m *Mutex) Lock() {
// 快速路徑:嘗試原子操作獲取鎖
if atomic.CompareAndSwapInt32(&m.state, 0, mutexLocked) {
return
}
// 慢速路徑:鎖已被持有,需要等待
m.lockSlow()
}- 讀寫均衡或者寫者較多的場(chǎng)景,使用這個(gè)鎖(防止寫者競(jìng)爭(zhēng),內(nèi)存占用也更小)
RWMutex
golang提供的讀寫鎖,和上面的鎖區(qū)別是區(qū)分了讀和寫兩種場(chǎng)景。讀之間的沖突不阻塞,若寫鎖遇到了讀鎖,需要等待所有讀者釋放,若讀者和寫者同時(shí)到達(dá),讀者要等待寫者完成(寫優(yōu)先)
func (rw *RWMutex) Lock() {
// 1. 獲取互斥鎖
rw.w.Lock()
// 2. 設(shè)置readerCount為負(fù)值,阻止新讀者
r := atomic.AddInt32(&rw.readerCount, -rwmutexMaxReaders)
// 3. 等待現(xiàn)有讀者
if r != 0 {
runtime_Semacquire(&rw.writerSem)
}
}
func (rw *RWMutex) RLock() {
// 檢查是否有寫者(readerCount < 0表示有寫者)
if atomic.AddInt32(&rw.readerCount, 1) < 0 {
// 有寫者在等待或正在寫,讀者必須阻塞
runtime_Semacquire(&rw.readerSem)
}
}- 讀多寫少的場(chǎng)景使用這個(gè)讀寫鎖,性能會(huì)有比較大的提升
WaitGroup
waitGroup提供了下面三個(gè)方法,用來(lái)控制多個(gè)goroutine是否都執(zhí)行完成,主goroutine調(diào)用Add方法設(shè)置要等待的goroutine數(shù)量,每個(gè)goroutine結(jié)束的時(shí)候,調(diào)用Done方法標(biāo)記這個(gè)任務(wù)結(jié)束,Wait方法會(huì)等待所有g(shù)oroutine調(diào)用完Done方法
func main() {
var wg sync.WaitGroup
wg.Add(1) // 增加一個(gè)等待者
go func() {
defer wg.Done()
// 執(zhí)行邏輯, 邏輯執(zhí)行完之后, 等待者數(shù)量-1
}()
wg.Wait() // 等待所有等待者執(zhí)行完成
}- 底層實(shí)現(xiàn)如下,它使用了一個(gè)state來(lái)存儲(chǔ)等待者和要執(zhí)行的工作數(shù)量
type WaitGroup struct {
noCopy noCopy
// 64位值的高32位是計(jì)數(shù)器,低32位是等待者數(shù)量
// 64位原子操作需要64位對(duì)齊,但32位編譯器不能確保這一點(diǎn)
state atomic.Uint64 // high 32 bits are counter, low 32 bits are waiter count.
}// WaitGroup狀態(tài)布局(64位)
// 高32位:計(jì)數(shù)器 (counter)
// 低32位:等待者數(shù)量 (waiters)
func (wg *WaitGroup) Add(delta int) {
statep, semap := wg.state()
// 更新計(jì)數(shù)器
state := atomic.AddUint64(statep, uint64(delta)<<32)
// 檢查狀態(tài)變化
v := int32(state >> 32) // 計(jì)數(shù)器
w := uint32(state) // 等待者數(shù)量
if v > 0 || w == 0 {
return // 還有工作要做,或者沒(méi)有等待者
}
// 所有工作完成,喚醒等待者
if *statep != 0 {
panic("sync: WaitGroup misuse")
}
// 喚醒所有等待的goroutine
for ; w != 0; w-- {
runtime_Semrelease(semap, false, 0)
}
}具體
- 高32位 = 工作計(jì)數(shù)器
wg.Add(5) // 高32位 += 5,表示還有5個(gè)工作要做
wg.Done() // 高32位 -= 1,表示完成了1個(gè)工作
- 當(dāng)高32位變?yōu)?時(shí),表示所有工作完成
- 低32位 = 等待者數(shù)量
wg.Wait() // 低32位 += 1,表示1個(gè)goroutine開(kāi)始等待
- 當(dāng)高32位變?yōu)?時(shí),低32位表示需要喚醒多少個(gè)goroutine
當(dāng)調(diào)用Wait方法時(shí),也是一個(gè)自旋等待的邏輯。配合cas,實(shí)現(xiàn)等待所有工作完成。具體細(xì)節(jié)參考源碼,這里只簡(jiǎn)單介紹下原理
Once
sync.Once{}提供了一個(gè)多次調(diào)用,只執(zhí)行一次的方法實(shí)現(xiàn),如下所示
func main() {
once := sync.Once{}
once.Do(func() {
// 這里的邏輯只會(huì)執(zhí)行一次
})
}- 這個(gè)的底層實(shí)現(xiàn)比較簡(jiǎn)單,因?yàn)閛nce內(nèi)部維護(hù)了一個(gè)原子類,指向一個(gè)32位無(wú)符號(hào)數(shù),當(dāng)執(zhí)行
func的時(shí)候,如果有多個(gè)goroutine并發(fā)執(zhí)行,兩個(gè)goroutine會(huì)競(jìng)爭(zhēng)鎖,最終只有一個(gè)能夠執(zhí)行func的邏輯,執(zhí)行完之后,這個(gè)數(shù)會(huì)設(shè)置為1,從而后面的所有g(shù)oroutine都不會(huì)再走到執(zhí)行邏輯。具體參考下面源碼實(shí)現(xiàn)
func (o *Once) Do(f func()) {
if o.done.Load() == 0 {
o.doSlow(f)
}
}
func (o *Once) doSlow(f func()) {
o.m.Lock()
defer o.m.Unlock()
if o.done.Load() == 0 {
defer o.done.Store(1)
f()
}
}Cond
- 這個(gè)工具主要是協(xié)調(diào)訪問(wèn)共享資源的那些
goroutine,c.L是工具內(nèi)部的一個(gè)鎖。當(dāng)調(diào)用c.L.Wait()會(huì)解鎖等待Signal或者Broadcast信號(hào),Broadcast會(huì)喚醒所有等待的Wait,Signal會(huì)隨機(jī)喚醒一個(gè)等待的Wait。具體使用方法如下面的例子所示
func main() {
cond := sync.NewCond(new(sync.Mutex))
done := false
read := func(name string, c *sync.Cond) {
c.L.Lock()
if !done {
c.Wait()
}
fmt.Println("start reading: ", name)
c.L.Unlock()
}
write := func(name string, c *sync.Cond) {
fmt.Println("start writing: ", name)
c.L.Lock()
done = true
c.L.Unlock()
fmt.Println("wakes all:", name)
c.Signal()
}
go read("read3", cond)
go read("read1", cond)
go read("read2", cond)
write("write1", cond)
time.Sleep(5 * time.Second)
}Map
- golang在sync包內(nèi)提供了一個(gè)并發(fā)安全的Map,可以增刪改查,遍歷、compareAndSwap、LoadOrStore等操作??梢宰鲆恍o(wú)鎖編程,提升性能
- 讀多寫少(讀操作占比>80%)、數(shù)據(jù)量大(鍵值對(duì)數(shù)量>1000)、寫入不頻繁(寫入頻率<100次/秒)、簡(jiǎn)單鍵值等場(chǎng)景,使用這個(gè)的性能要優(yōu)于普通map+鎖
Pool
- 對(duì)于頻繁進(jìn)行內(nèi)存分配和回收的場(chǎng)景,可以使用
sync.Pool{},例如下面的場(chǎng)景
// 頻繁分配和回收的典型特征
func frequentAllocation() {
pool := sync.Pool{
New: func() interface{} {
return make([]byte, 1024)
},
}
// 在循環(huán)中重復(fù)創(chuàng)建和銷毀對(duì)象
for i := 0; i < 1000; i++ {
// 分配:創(chuàng)建新對(duì)象
// buf := make([]byte, 1024) // 直接分配內(nèi)存
buf := pool.Get().([]byte) // 使用對(duì)象池
// 使用對(duì)象
copy(buf, data[i])
process(buf)
// 回收:對(duì)象超出作用域,被GC回收
pool.Put(buf[:0]) // 歸還對(duì)象(清空)
}
// 每次循環(huán)都經(jīng)歷:分配→使用→回收
}到此這篇關(guān)于深度剖析golang中的sync包的文章就介紹到這了,更多相關(guān)goland sync包內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
Golang 端口復(fù)用測(cè)試的實(shí)現(xiàn)
這篇文章主要介紹了Golang 端口復(fù)用測(cè)試的實(shí)現(xiàn),文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2021-03-03
Linux環(huán)境下編譯并運(yùn)行g(shù)o項(xiàng)目的全過(guò)程
Go語(yǔ)言是Google的開(kāi)源編程語(yǔ)言,廣泛應(yīng)用于云計(jì)算、分布式系統(tǒng)開(kāi)發(fā)等領(lǐng)域,在Linux上也有大量的應(yīng)用場(chǎng)景,這篇文章主要給大家介紹了關(guān)于Linux環(huán)境下編譯并運(yùn)行g(shù)o項(xiàng)目的相關(guān)資料,需要的朋友可以參考下2023-11-11
Golang中文件目錄操作的實(shí)現(xiàn)步驟詳解
在Golang中,文件目錄是指計(jì)算機(jī)文件系統(tǒng)中的文件夾或目錄。目錄是用于組織和存儲(chǔ)文件的一種方式,可以包含文件和其他子目錄,本文主要介紹了Golang中文件目錄操作的實(shí)現(xiàn)方法,需要的朋友可以參考下2023-05-05
在 Golang 中實(shí)現(xiàn) Cache::remember 方法詳解
這篇文章主要介紹了在 Golang 中實(shí)現(xiàn) Cache::remember 方法詳解,本文給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2021-03-03
Golang實(shí)現(xiàn)讀取ZIP壓縮包并顯示Gin靜態(tài)html網(wǎng)站
這篇文章主要為大家詳細(xì)介紹了如何通過(guò)Golang實(shí)現(xiàn)從ZIP壓縮包讀取內(nèi)容并作為Gin靜態(tài)網(wǎng)站顯示,感興趣的小伙伴可以跟隨小編一起學(xué)習(xí)一下2025-07-07
go進(jìn)行http請(qǐng)求偶發(fā)EOF問(wèn)題分析
go使用連接池進(jìn)行http請(qǐng)求,一般都能請(qǐng)求成功,但偶然會(huì)出現(xiàn)請(qǐng)求失敗返回EOF錯(cuò)誤的情況,本文主要來(lái)帶大家分析一下為什么會(huì)出現(xiàn)這樣的問(wèn)題并提供解決方法,需要的可以參考下2025-01-01

