網站首頁 編程語言 正文
GO的鎖和原子操作分享
上次我們說到協程,我們再來回顧一下:
- 協程類似線程,是一種更為輕量級的調度單位
- 線程是系統級實現的,常見的調度方法是時間片輪轉法
- 協程是應用軟件級實現,原理與線程類似
- 協程的調度基于 GPM 模型實現
要是對協程的使用感興趣的話,可以看看這篇文章簡單了解一下瞅一眼就會使用GO的并發編程分享
今天我們來聊聊GO里面的鎖
鎖是什么
鎖 是用于解決隔離性的一種機制
某個協程(線程)在訪問某個資源時先鎖住,防止其它協程的訪問,等訪問完畢解鎖后其他協程再來加鎖進行訪問
在我們生活中,我們應該不會陌生,鎖是這樣的
本意是指置于可啟閉的器物上,以鑰匙或暗碼開啟,引申義是用鎖鎖住、封閉
生活中用到的鎖
上鎖基本是為了防止外人進來、防止自己財物被盜
編程語言中的鎖
鎖的種類更是多種多樣,每種鎖的加鎖開銷以及應用場景也不盡相同
鎖是用來做什么的
用來控制各個協程的同步,防止資源競爭導致錯亂問題
在高并發的場景下,如果選對了合適的鎖,則會大大提高系統的性能,否則性能會降低。
那么知道各種鎖的開銷,以及應用場景很有必要
GO中的鎖有哪些?
- 互斥鎖
- 讀寫鎖
我們在編碼中會存在多個 goroutine 協程同時操作一個資源(臨界區),這種情況會發生競態問題(數據競態)
舉一個生活中的例子
生活中最明顯的例子就是,大家搶著上廁所,資源有限,只能一個一個的用
舉一個編碼中的例子
package main import ( "fmt" "sync" ) // 全局變量 var num int64 var wg sync.WaitGroup func add() { for i := 0; i < 10000000; i++ { num = num + 1 } // 協程退出, 記錄 -1 wg.Done() } func main() { // 啟動2個協程,記錄 2 wg.Add(2) go add() go add() // 等待子協程退出 wg.Wait() fmt.Println(num) }
按照上述代碼,我們的輸出結果應該是 20000000
,每一個協程計算 10000000 次,可是實際結果卻是
10378923
每一次計算的結果還不一樣,出現這個問題的原因就是上述提到的資源競爭
兩個 goroutine 協程在訪問和修改num變量,會存在2個協程同時對num+1 , 最終num 總共只加了 1 ,而不是 2
這就導致最后的結果與期待的不符,那么我們如何解決呢?
我們當然是用鎖控制同步了,保證各自協程在操作臨界區資源的時候,先確實是否拿到鎖,只有拿到鎖了才能進行對臨界區資源的修改
先來看看互斥鎖
互斥鎖
互斥鎖的簡單理解就像上述我們講到上廁所的案例一樣,同一時間點,只能有一個人在使用其他人只能排隊等待
在編程中,引入了對象互斥鎖的概念,來保證共享數據操作的完整性
每個對象都對應于一個可稱為互斥鎖的標記,這個標記用來保證在任一時刻,只能有一個協程訪問該對象。
應用場景
寫大于讀操作的
它代表的資源就是一個,不管是讀者還是寫者,只要誰擁有了它,那么其他人就只有等待解鎖后
我們來使用互斥鎖解決上述的問題
互斥鎖 - 解決問題
互斥鎖是一種常用的控制共享資源訪問的方法,它能夠保證同時只有一個 goroutine 協程可以訪問共享資源
Go 中使用到如下 1個知識點來解決
sync包 的 Mutex類型 來實現互斥鎖
package main import ( "fmt" "sync" ) // 全局變量 var num int64 var wg sync.WaitGroup var lock sync.Mutex func add() { for i := 0; i < 10000000; i++ { // 訪問資源前 加鎖 lock.Lock() num = num + 1 // 訪問資源后 解鎖 lock.Unlock() } // 協程退出, 記錄 -1 wg.Done() } func main() { // 啟動2個協程,記錄 2 wg.Add(2) go add() go add() // 等待子協程退出 wg.Wait() fmt.Println(num) }
執行上述代碼,我們能看到,輸出的結果與我們預期的一致
20000000
使用互斥鎖能夠保證同一時間有且只有一個goroutine 協程進入臨界區,其他的goroutine則在等待鎖
當互斥鎖釋放后,等待的 goroutine 協程才可以獲取鎖進入臨界區
如何知道哪一個協程是先被喚醒呢?
可是,多個goroutine 協程同時等待一個鎖時,如何知道哪一個協程是先被喚醒呢?
互斥鎖這里的喚醒的策略是隨機的,并不知道到底是先喚醒誰
讀寫鎖
為什么有了互斥鎖 ,還要讀寫鎖呢?
很明顯就是互斥鎖不能滿足所有的應用場景,就催生出了讀寫鎖,我們細細道來
互斥鎖是完全互斥的,不管協程是讀臨界區資源還是寫臨界區資源,都必須要拿到鎖,否則就無法操作(這個限制太死了對嗎)
可是在我們實際的應用場景下是讀多寫少
若我們并發的去讀取一個資源,且不對資源做任何修改的時候如果也要加鎖才能讀取數據,是不是就很沒有必要呢
這種場景下讀寫鎖就發揮作用了,他就相對靈活了,也很好的解決了讀多寫少的場景問題
讀寫鎖的種類
- 讀鎖
- 寫鎖
當一個goroutine 協程獲取讀鎖之后,其他的 goroutine 協程如果是獲取讀鎖會繼續獲得鎖
可如果是獲取寫鎖就必須等待
當一個 goroutine 協程獲取寫鎖之后,其他的goroutine 協程無論是獲取讀鎖還是寫鎖都會等待
我們先來寫一個讀寫鎖的DEMO
Go 中使用到如下 1個知識點來解決
sync包 的 RWMutex類型 來實現讀寫鎖
package main import ( "fmt" "sync" "time" ) var ( num int64 wg sync.WaitGroup //lock sync.Mutex rwlock sync.RWMutex ) func write() { // 加互斥鎖 // lock.Lock() // 加寫鎖 rwlock.Lock() num = num + 1 // 模擬真實寫數據消耗的時間 time.Sleep(10 * time.Millisecond) // 解寫鎖 rwlock.Unlock() // 解互斥鎖 // lock.Unlock() // 退出協程前 記錄 -1 wg.Done() } func read() { // 加互斥鎖 // lock.Lock() // 加讀鎖 rwlock.RLock() // 模擬真實讀取數據消耗的時間 time.Sleep(time.Millisecond) // 解讀鎖 rwlock.RUnlock() // 解互斥鎖 // lock.Unlock() // 退出協程前 記錄 -1 wg.Done() } func main() { // 用于計算時間 消耗 start := time.Now() // 開5個協程用作 寫 for i := 0; i < 5; i++ { wg.Add(1) go write() } // 開500 個協程,用作讀 for i := 0; i < 1000; i++ { wg.Add(1) go read() } // 等待子協程退出 wg.Wait() end := time.Now() // 打印程序消耗的時間 fmt.Println(end.Sub(start)) }
我們開5個協程用于寫,開1000個協程用于讀,使用讀寫鎖加鎖,結果耗時 54.4871ms
如下
54.4871ms
如果我們將上述代碼修改成加 互斥鎖,運行之后的結果是 1.7750029s
如下
1.7750029s
是不是結果相差很大呢,對于不同的場景應用不同的鎖,對于我們的程序性能影響也是很大,當然上述結果,若讀協程,和寫協程的個數差距越大,結果就會越懸殊
我們總結一下這一小塊的邏輯:
- 寫者是排他性的,一個讀寫鎖同時只能有一個寫者或多個讀者
- 不能同時既有讀者又有寫者
- 如果讀寫鎖當前沒有讀者,也沒有寫者,那么寫者可以立刻獲得讀寫鎖,否則它必須自旋在那里,直到沒有任何寫者或讀者。
- 如果讀寫鎖沒有寫者,那么讀者可以立即獲得該讀寫鎖,否則讀者必須自旋在那里,直到寫者釋放該讀寫鎖。
上述提了自旋鎖,我們來簡單解釋一下,什么是自旋鎖
自旋鎖是專為防止多處理器并發而引入的一種鎖,它在內核中大量應用于中斷處理等部分(對于單處理器來說,防止中斷處理中的并發可簡單采用關閉中斷的方式,即在標志寄存器中關閉/打開中斷標志位,不需要自旋鎖)。
簡單來說,在并發過程中,若其中一個協程拿不到鎖,他會不停的去嘗試拿鎖,不停的去看能不能拿,而不是阻塞睡眠
自旋鎖和互斥鎖的區別
互斥鎖
當拿不到鎖的時候,會阻塞等待,會睡眠,等待鎖釋放后被喚醒
自旋鎖
當拿不到鎖的時候,會在原地不停的看能不能拿到鎖,所以叫做自旋,他不會阻塞,不會睡眠
如何選擇鎖
對于 C/C++ 而言
- 若加鎖后的業務操作消耗,大于互斥鎖阻塞后切換上下文的消耗 ,那么就選擇互斥鎖
- 若加鎖后的業務操作消耗,小于互斥鎖阻塞后切換上下文的消耗,那么選擇自旋鎖
對于 GO 而言
- 若寫的頻次大大的多余讀的頻次,那么選擇互斥鎖
- 若讀的頻次大大的多余寫的頻次,那么選擇讀寫鎖
我們都是對自身要求比較高的同學,那么有沒有比鎖還好用的東西呢?
自然是有的,我們來看看原子操作
啥是原子操作
"原子操作(atomic operation)是不需要synchronized",這是多線程編程的老生常談了。所謂原子操作是指不會被線程調度機制打斷的操作
這種操作一旦開始,就一直運行到結束,中間不會有任何 context switch (切換到另一個線程)。
原子操作的特性:
原子操作是不可分割的,在執行完畢之前不會被任何其它任務或事件中斷
上述我們的加鎖案例,咱們編碼中的加鎖操作會涉及內核態的上下文切換會比較耗時、代價比較高
針對基本的數據類型我們還可以使用原子操作來保證并發安全
因為原子操作是Go語言提供的方法它在用戶態就可以完成,因此性能比加鎖操作更好
不用我們自己寫匯編,這里 GO 也提供了原子操作的包,供我們一起來使用 sync/atomic
我們對上述的案例做一個延伸
package main import ( "fmt" "sync" "sync/atomic" "time" ) var num int64 var l sync.Mutex var wg sync.WaitGroup // 普通版加函數 func add() { num = num + 1 wg.Done() } // 互斥鎖版加函數 func mutexAdd() { l.Lock() num = num + 1 l.Unlock() wg.Done() } // 原子操作版加函數 func atomicAdd() { atomic.AddInt64(&num, 1) wg.Done() } func main() { // 目的是 記錄程序消耗時間 start := time.Now() for i := 0; i < 20000; i++ { wg.Add(1) // go add() // 無鎖的 add函數 不是并發安全的 // go mutexAdd() // 互斥鎖的 add函數 是并發安全的,因為拿不到互斥鎖會阻塞,所以加鎖性能開銷大 go atomicAdd() // 原子操作的 add函數 是并發安全,性能優于加鎖的 } // 等待子協程 退出 wg.Wait() end := time.Now() fmt.Println(num) // 打印程序消耗時間 fmt.Println(end.Sub(start)) }
我們使用上述 demo 代碼,模擬了3種情況下,程序的耗時以及計算結果對比
不加鎖
無鎖的 add函數 不是并發安全的
19495
11.9474ms
加互斥鎖
互斥鎖的 add函數 是并發安全的,因為拿不到互斥鎖會阻塞,所以加鎖性能開銷大
20000
14.9586ms
使用原子操作
原子操作的 add函數 是并發安全,性能優于加鎖的
20000
9.9726ms
總結
- 分享了鎖是什么,用來做什么
- 分享了互斥鎖,讀寫鎖,以及其區別和應用場景
- 分享了原子操作
- 大家感興趣可以去看看鎖的實現,里面也是有使用原子操作
原文鏈接:https://juejin.cn/post/6972846349968474142
- 上一篇:沒有了
- 下一篇:沒有了
相關推薦
- 2023-01-09 Redis排序命令Sort深入解析_Redis
- 2022-08-10 etcd通信接口之客戶端API核心方法實戰_Golang
- 2022-02-24 TypeError: ‘Serializer‘ object is not callable
- 2022-04-02 ?Python錯誤與異常處理_python
- 2022-05-09 關于Ajax的疑難雜癥詳解_AJAX相關
- 2022-10-03 Pandas中inf值替換的方法_python
- 2022-09-24 python中的[1:]、[::-1]、X[:,m:n]和X[1,:]的使用_python
- 2023-05-29 SQLSERVER?語句交錯引發的死鎖問題案例詳解_MsSql
- 欄目分類
-
- 最近更新
-
- window11 系統安裝 yarn
- 超詳細win安裝深度學習環境2025年最新版(
- Linux 中運行的top命令 怎么退出?
- MySQL 中decimal 的用法? 存儲小
- get 、set 、toString 方法的使
- @Resource和 @Autowired注解
- Java基礎操作-- 運算符,流程控制 Flo
- 1. Int 和Integer 的區別,Jav
- spring @retryable不生效的一種
- Spring Security之認證信息的處理
- Spring Security之認證過濾器
- Spring Security概述快速入門
- Spring Security之配置體系
- 【SpringBoot】SpringCache
- Spring Security之基于方法配置權
- redisson分布式鎖中waittime的設
- maven:解決release錯誤:Artif
- restTemplate使用總結
- Spring Security之安全異常處理
- MybatisPlus優雅實現加密?
- Spring ioc容器與Bean的生命周期。
- 【探索SpringCloud】服務發現-Nac
- Spring Security之基于HttpR
- Redis 底層數據結構-簡單動態字符串(SD
- arthas操作spring被代理目標對象命令
- Spring中的單例模式應用詳解
- 聊聊消息隊列,發送消息的4種方式
- bootspring第三方資源配置管理
- GIT同步修改后的遠程分支