Go 語言入門很簡單:讀寫鎖

      網友投稿 785 2022-05-30

      前言

      在上一篇文章中,我們介紹了 Go 互斥鎖,這一篇文章我們來介紹 Go 語言幫我們實現的標準庫的 sync.RWMutex{} 讀寫鎖。

      通過使用 sync.RWMutex,我們的程序變得更加高效。

      什么是讀者-寫者問題

      先來了解讀者-寫者問題(Readers–writers problem)的背景。最基本的讀者-寫者問題首先由 Courtois 等人提出并解決。

      讀者-寫者問題描述了計算機并發處理讀寫數據遇到的問題,如何保證數據完整性、一致性。解決讀者-寫者問題需保證對于一份資源操作滿足以下下條件:

      讀寫互斥

      寫寫互斥

      允許多個讀者同時讀取

      解決讀者-寫者問題,可以采用讀者優先(readers-preference)方案或者寫者優先(writers-preference)方案。

      讀者優先(readers-preference):讀者優先是讀操作優先于寫操作,即使寫操作提出申請資源,但只要還有讀者在讀取操作,就還允許其他讀者繼續讀取操作,直到所有讀者結束讀取,才開始寫。讀優先可以提供很高的并發處理性能,但是在頻繁讀取的系統中,會長時間寫阻塞,導致寫饑餓。

      寫者優先(writers-preference):寫者優先是寫操作優先于讀操作,如果有寫者提出申請資源,在申請之前已經開始讀取操作的可以繼續執行讀取,但是如果再有讀者申請讀取操作,則不能夠讀取,只有在所有的寫者寫完之后才可以讀取。寫者優先解決了讀者優先造成寫饑餓的問題。但是若在頻繁寫入的系統中,會長時間讀阻塞,導致讀饑餓。

      RWMutex設計采用寫者優先方法,保證寫操作優先處理。

      回顧一下互斥鎖的案例

      多次單筆存款

      假設你有一個銀行賬戶,那你既可以進行存錢,也可以查詢余額的操作。

      package main import "fmt" type Account struct { name string balance float64 } // func (a *Account) Deposit(amount float64) { a.balance += amount } func (a *Account) Balance() float64 { return a.balance } func main() { user := &Account{"xiaoW", 0} user.Deposit(10000) user.Deposit(200) user.Deposit(2022) fmt.Printf("%s's account balance has %.2f $.", user.name, user.Balance()) }

      執行該代碼,進行三筆存款,我們可以看到輸出的賬戶余額為 12222.00 $:

      $ go run main.go xiaoW's account balance has 12222.00 $.

      同時多次存款

      但如果我們進行同時存款呢?即使用 goroutine 來生成三個線程來模擬同時存款的操作。然后利用sync.WaitGroup 去等待所有 goroutine 執行完畢,打印最后的余額:

      package main import ( "fmt" "sync" ) type Account struct { name string balance float64 } func (a *Account) Deposit(amount float64) { a.balance += amount } func (a *Account) Balance() float64 { return a.balance } func main() { var wg sync.WaitGroup user := &Account{"xiaoW", 0} wg.Add(3) go func() { user.Deposit(10000) wg.Done() }() go func() { user.Deposit(200) wg.Done() }() go func() { user.Deposit(2022) wg.Done() }() wg.Wait() fmt.Printf("%s's account banlance has %.2f $.", user.name, user.Balance()) }

      同時執行 3 次是沒問題的,但如果執行 1000 次呢?

      package main import ( "fmt" "sync" ) type Account struct { name string balance float64 } // func (a *Account) Deposit(amount float64) { a.balance += amount } func (a *Account) Balance() float64 { return a.balance } func main() { var wg sync.WaitGroup user := &Account{"xiaoW", 0} n := 1000 wg.Add(n) for i := 1; i <= n; i++ { go func() { user.Deposit(1000) wg.Done() }() } wg.Wait() fmt.Printf("%s's account banlance has %.2f $.", user.name, user.Balance()) }

      我們多次運行該程序,發現每次運行結果都不一樣。

      $ go run main.go xiaoW's account banlance has 0.00 $. $ go run main.go xiaoW's account banlance has 886000.00 $. $ go run main.go xiaoW's account banlance has 2000.00 $.

      正常的結果應該為 1000 * 1000 = 1000000.00 的余額才對,運行很多次的情況下才能看到一次正常的結果。

      xiaoW's account banlance has 1000000.00 $.

      使用 -race 參數來查看數據競爭

      我們可以利用 -race 參數來查看我們的代碼是否有競爭:

      $ go run -race main.go ================== WARNING: DATA RACE Read at 0x00c00000e040 by goroutine 7: main.(*Account).Deposit() /home/wade/GoProjects/Go RWMutex/v2/main.go:15 +0x48 main.main.func1() /home/wade/GoProjects/Go RWMutex/v2/main.go:31 +0x36 Previous write at 0x00c00000e040 by goroutine 45: main.(*Account).Deposit() /home/wade/GoProjects/Go RWMutex/v2/main.go:15 +0x6e main.main.func1() /home/wade/GoProjects/Go RWMutex/v2/main.go:31 +0x36 Goroutine 7 (running) created at: main.main() /home/wade/GoProjects/Go RWMutex/v2/main.go:30 +0x144 Goroutine 45 (finished) created at: main.main() /home/wade/GoProjects/Go RWMutex/v2/main.go:30 +0x144 ================== xiaoW's account banlance has 996000.00 $.Found 1 data race(s) exit status 66

      我們可以看到了發生了 goroutine 的線程競爭,goroutine 7 在讀的時候,goroutine 45 在寫,最終導致了讀寫不一致,所以最終的余額也都不符合我們的預期。

      互斥鎖:sync.Mutex

      對于上述發生的線程競爭問題,我們就可以使用互斥鎖來解決,即同一時間只能有一個 goroutine 能夠處理該函數。代碼改正如下:

      package main import ( "fmt" "sync" ) type Account struct { name string balance float64 mux sync.Mutex } // func (a *Account) Deposit(amount float64) { a.mux.Lock() // lock a.balance += amount a.mux.Unlock() // unlock } func (a *Account) Balance() float64 { return a.balance } func main() { var wg sync.WaitGroup user := &Account{} user.name = "xiaoW" n := 1000 wg.Add(n) for i := 1; i <= n; i++ { go func() { user.Deposit(1000) wg.Done() }() } wg.Wait() fmt.Printf("%s's account banlance has %.2f $.", user.name, user.Balance()) }

      此時,我們再運行 3次 go run -race main.go,得到統一的結果:

      $ go run -race main.go xiaoW's account banlance has 1000000.00 $. $ go run -race main.go xiaoW's account banlance has 1000000.00 $. $ go run -race main.go xiaoW's account banlance has 1000000.00 $.

      讀和寫同時進行

      雖然我們同一時間存款問題通過互斥鎖得到了解決。但是如果同時存款與查詢余額呢?

      package main import ( "fmt" "sync" ) type Account struct { name string balance float64 mux sync.Mutex } // func (a *Account) Deposit(amount float64) { a.mux.Lock() // lock a.balance += amount a.mux.Unlock() // unlock } func (a *Account) Balance() float64 { return a.balance } func main() { var wg sync.WaitGroup user := &Account{} user.name = "xiaoW" n := 1000 wg.Add(n) for i := 1; i <= n; i++ { go func() { user.Deposit(1000) wg.Done() }() } // 查詢余額 wg.Add(n) for i := 0; i <= n; i++ { go func() { _ = user.Balance() wg.Done() }() } wg.Wait() fmt.Printf("%s's account banlance has %.2f $.", user.name, user.Balance()) }

      然后我們運行代碼,就又出現了線程競爭的問題:

      $ go run -race main.go ================== WARNING: DATA RACE Read at 0x00c0000ba010 by goroutine 73: main.(*Account).Balance() /home/wade/GoProjects/Go RWMutex/v2/main.go:22 +0x44 main.main.func2() /home/wade/GoProjects/Go RWMutex/v2/main.go:43 +0x32 Previous write at 0x00c0000ba010 by goroutine 72: main.(*Account).Deposit() /home/wade/GoProjects/Go RWMutex/v2/main.go:17 +0x84 main.main.func1() /home/wade/GoProjects/Go RWMutex/v2/main.go:35 +0x46 Goroutine 73 (running) created at: main.main() /home/wade/GoProjects/Go RWMutex/v2/main.go:42 +0x1ba Goroutine 72 (finished) created at: main.main() /home/wade/GoProjects/Go RWMutex/v2/main.go:34 +0x15e ================== panic: sync: negative WaitGroup counter goroutine 2018 [running]: sync.(*WaitGroup).Add(0xc0000b4010, 0xffffffffffffffff) /usr/local/go/src/sync/waitgroup.go:74 +0x2e5 sync.(*WaitGroup).Done(...) /usr/local/go/src/sync/waitgroup.go:99 main.main.func2(0xc0000ba000, 0xc0000b4010) /home/wade/GoProjects/Go RWMutex/v2/main.go:44 +0x5d created by main.main /home/wade/GoProjects/Go RWMutex/v2/main.go:42 +0x1bb exit status 2

      同理,我們需要對查詢余額作同樣的加鎖處理:

      func (a *Account) Balance() (balance float64) { a.mux.Lock() balance = a.balance a.mux.Unlock() return balance }

      如果發生讀寫阻塞呢?我們利用 time.Sleep() 來模擬線程阻塞的過程:

      package main import ( "log" "sync" "time" ) type Account struct { balance float64 mux sync.Mutex } // func (a *Account) Deposit(amount float64) { a.mux.Lock() // lock time.Sleep(time.Second * 2) a.balance += amount a.mux.Unlock() // unlock } func (a *Account) Balance() (balance float64) { a.mux.Lock() time.Sleep(time.Second * 2) balance = a.balance a.mux.Unlock() return balance } func main() { wg := &sync.WaitGroup{} user := &Account{} n := 5 wg.Add(n) for i := 1; i <= n; i++ { go func() { user.Deposit(1000) log.Printf("寫:存款: %v", 1000) wg.Done() }() } wg.Add(n) for i := 0; i <= n; i++ { go func() { log.Printf("讀:余額: %v", user.Balance()) wg.Done() }() } wg.Wait() }

      我們在程序中,每隔兩秒處理一次存款和查詢操作,總共發生 5 次存款和 5 次查詢,那么就需要 20 秒來執行這個程序。如果存款可以接受 2 秒的時間,但是讀取應該只需要更快才對,即查詢操作不應該發生阻塞。

      $ go run -race main.go 2022/02/28 14:31:43 寫:存款: 1000 2022/02/28 14:31:45 寫:存款: 1000 2022/02/28 14:31:47 寫:存款: 1000 2022/02/28 14:31:49 寫:存款: 1000 2022/02/28 14:31:51 寫:存款: 1000 2022/02/28 14:31:53 讀:余額: 5000 2022/02/28 14:31:55 讀:余額: 5000 2022/02/28 14:31:57 讀:余額: 5000 2022/02/28 14:31:59 讀:余額: 5000 2022/02/28 14:32:01 讀:余額: 5000

      讀寫鎖:sync.RWMutex

      Mutex 將所有的 goroutine 視為平等的,并且只允許一個 goroutine 獲取鎖。針對這種情況,讀寫鎖就該被派上用場了。

      RWMutex 是 Go 語言中內置的一個 reader/writer 鎖,用來解決讀者-寫者問題(Readers–writers problem)。任意數量的讀取器可以同時獲取鎖,或者單個寫入器可以獲取鎖。 這個想法是讀者只關心與寫者的沖突,并且可以毫無困難地與其他讀者并發執行。

      Go 的讀寫鎖的特點:多讀單寫。 RWMutex 結構更靈活,支持兩類 goroutine:readers 和 writers。 在任意一時刻,一個 RWMutex 只能由任意數量的 readers 持有,或者只能由一個 writers 持有。

      讀寫鎖的四個方法

      RLock():此方法嘗試獲取讀鎖,并會阻塞直到被獲取

      RUnlock():解鎖讀鎖

      Lock():獲取寫鎖,阻塞直到被獲取

      UnLock():釋放寫鎖

      RLocker():該方法返回一個指向 Locker 的指針,用于獲取和釋放讀鎖

      讀寫鎖演示

      把互斥鎖改為讀寫鎖也很簡單,只需要把 sync.Mutex 換成 sync.RWMutex ,然后在讀操作的地方改為 RLock(),釋放讀鎖改為 RUnlock():

      package main import ( "log" "sync" "time" ) type Account struct { balance float64 mux sync.RWMutex // 讀寫鎖 } // func (a *Account) Deposit(amount float64) { a.mux.Lock() // 寫鎖 time.Sleep(time.Second * 2) a.balance += amount a.mux.Unlock() // 釋放寫鎖 } func (a *Account) Balance() (balance float64) { a.mux.RLock() // 讀鎖 time.Sleep(time.Second * 2) balance = a.balance a.mux.RUnlock() // 釋放讀鎖 return balance } func main() { wg := &sync.WaitGroup{} user := &Account{} n := 5 wg.Add(n) for i := 1; i <= n; i++ { go func() { user.Deposit(1000) log.Printf("寫:存款: %v", 1000) wg.Done() }() } wg.Add(n) for i := 0; i <= n; i++ { go func() { log.Printf("讀:余額: %v", user.Balance()) wg.Done() }() } wg.Wait() }

      明顯能感覺到讀操作變快了,發生一次寫之后,直接發生 6 次讀操作,說明讀操作是同時進行的,存款1000 一次后,6 次讀操作都是 1000 元,說明結果是正確的。

      2022/02/28 14:42:50 寫:存款: 1000 2022/02/28 14:42:52 讀:余額: 1000 2022/02/28 14:42:52 讀:余額: 1000 2022/02/28 14:42:52 讀:余額: 1000 2022/02/28 14:42:52 讀:余額: 1000 2022/02/28 14:42:52 讀:余額: 1000 2022/02/28 14:42:52 讀:余額: 1000 2022/02/28 14:42:54 寫:存款: 1000 2022/02/28 14:42:56 寫:存款: 1000 2022/02/28 14:42:58 寫:存款: 1000

      總結

      本文從讀者-寫者問題出發,回顧了互斥鎖的案例:一個銀行賬戶存款和查詢的競爭問題的出現以及解決方法。最后引出 Go 自帶的讀寫鎖 sync.RWMutex 。

      讀寫鎖的特點是多讀單寫,一個 RWMutex 只能由任意數量的 readers 持有,或者只能由一個 writers 持有。我們可以利用讀寫鎖來鎖定某個操作以防止其他例程/線程在處理它時更改值,防止程序出現不可預測的錯誤。最后,可以利用讀寫鎖彌補互斥鎖的缺陷,用來加快程序的讀操作,減少程序的運行時間。

      靈感來源:

      Go 語言入門很簡單:讀寫鎖

      Go 程序設計語言--sync.RWMutex讀寫鎖

      stackoverflow -- How to use RWMutex

      官方文檔 -- RWMutex

      Golang RWMutex示例

      Using Mutexes in Golang - A Comprehensive Tutorial With Examples

      sync.RWMutex

      Go語言實戰筆記(十七)| Go 讀寫鎖

      Go 任務調度

      版權聲明:本文內容由網絡用戶投稿,版權歸原作者所有,本站不擁有其著作權,亦不承擔相應法律責任。如果您發現本站中有涉嫌抄襲或描述失實的內容,請聯系我們jiasou666@gmail.com 處理,核實后本網站將在24小時內刪除侵權內容。

      上一篇:【圖像分類】實戰——使用VGG16實現對植物幼苗的分類(pytroch)
      下一篇:淺談CSRF攻擊方式
      相關文章
      亚洲日本一区二区三区在线| 国外亚洲成AV人片在线观看| 国产成人精品日本亚洲| 日韩亚洲国产二区| 久久亚洲中文无码咪咪爱| 亚洲日韩精品无码专区| 亚洲一区二区三区高清不卡| 亚洲国产成人精品无码一区二区| 亚洲欧洲精品一区二区三区| 亚洲视频一区二区在线观看| 久久精品国产亚洲av高清漫画| 亚洲精品人成在线观看| 久久久久亚洲AV无码专区首JN | 亚洲日本国产精华液| 亚洲视频网站在线观看| 亚洲国产精品人久久电影| 亚洲午夜一区二区电影院| 亚洲一区二区久久| 亚洲第一成人在线| 亚洲国产一区二区三区在线观看| 日韩欧美亚洲国产精品字幕久久久| 国产亚洲欧美日韩亚洲中文色| 婷婷综合缴情亚洲狠狠尤物| 亚洲免费日韩无码系列| 国产亚洲欧洲Aⅴ综合一区| 亚洲精品无码Av人在线观看国产 | 黑人精品videos亚洲人| 亚洲91av视频| 久久精品国产亚洲AV久| 亚洲熟妇无码八V在线播放| 国内成人精品亚洲日本语音| 亚洲综合国产精品第一页| 国产亚洲精品一品区99热| 亚洲黄色免费电影| 亚洲国产av一区二区三区丶| 亚洲熟妇久久精品| 亚洲国产中文v高清在线观看| 国产亚洲色婷婷久久99精品| 久久久久亚洲AV无码观看| 亚洲中文字幕一区精品自拍| 亚洲av午夜精品一区二区三区 |