Go語言並發編程之互斥鎖Mutex和讀寫鎖RWMutex

在並發編程中,多個Goroutine訪問同一塊內存資源時可能會出現競態條件,我們需要在臨界區中使用適當的同步操作來以避免競態條件。Go 語言中提供瞭很多同步工具,本文將介紹互斥鎖Mutex和讀寫鎖RWMutex的使用方法。

一、互斥鎖Mutex

1、Mutex介紹

Go 語言的同步工具主要由 sync 包提供,互斥鎖 (Mutex) 與讀寫鎖 (RWMutex) 就是sync 包中的方法。

互斥鎖可以用來保護一個臨界區,保證同一時刻隻有一個 goroutine 處於該臨界區內。主要包括鎖定(Lock方法)和解鎖(Unlock方法)兩個操作,首先對進入臨界區的goroutine進行鎖定,離開時進行解鎖。

使用互斥鎖 (Mutex)時要註意以下幾點:

  • 不要重復鎖定互斥鎖,否則會阻塞,也可能會導致死鎖(deadlock);
  • 要對互斥鎖進行解鎖,這也是為瞭避免重復鎖定;
  • 不要對未鎖定或者已解鎖的互斥鎖解鎖;
  • 不要在多個函數之間直接傳遞互斥鎖,sync.Mutex類型屬於值類型,將它傳給一個函數時,會產生一個副本,在函數中對鎖的操作不會影響原鎖

總之,一個互斥鎖隻用來保護一個臨界區,加鎖後記得解鎖,對於每一個鎖定操作,都要有且隻有一個對應的解鎖操作,也就是加鎖和解鎖要成對出現,最保險的做法時使用 defer語句 解鎖。

2、Mutex使用實例

下面的代碼模擬取錢和存錢操作:

package main

import (
 "flag"
 "fmt"
 "sync"
)

var (
    mutex   sync.Mutex
    balance int
    protecting uint  // 是否加鎖
    sign = make(chan struct{}, 10) //通道,用於等待所有goroutine
)

// 存錢
func deposit(value int) {
    defer func() {
        sign <- struct{}{}
    }()

    if protecting == 1 {
        mutex.Lock()
        defer mutex.Unlock()
    }

    fmt.Printf("餘額: %d\n", balance)
    balance += value
    fmt.Printf("存 %d 後的餘額: %d\n", value, balance)
    fmt.Println()

}

// 取錢
func withdraw(value int) {
    defer func() {
        sign <- struct{}{}
    }()
    
    if protecting == 1 {
        mutex.Lock()
        defer mutex.Unlock()
    }

    fmt.Printf("餘額: %d\n", balance)
    balance -= value
    fmt.Printf("取 %d 後的餘額: %d\n", value, balance)
    fmt.Println()

}

func main() {
    
    for i:=0; i < 5; i++ {
        go withdraw(500) // 取500
        go deposit(500)  // 存500
    }

    for i := 0; i < 10; i++ {
  <-sign
 }
    fmt.Printf("當前餘額: %d\n", balance)
}

func init() {
    balance = 1000 // 初始賬戶餘額為1000
    flag.UintVar(&protecting, "protecting", 0, "是否加鎖,0表示不加鎖,1表示加鎖")
}

上面的代碼中,使用瞭通道來讓主 goroutine 等待其他 goroutine 運行結束,每個子goroutine在運行結束之前向通道發送一個元素,主 goroutine 在最後從這個通道接收元素,接收次數與子goroutine個數相同。接收完後就會退出主goroutine

代碼使用協程實現多次(5次)對一個賬戶進行存錢和取錢的操作,先來看不加鎖的情況:

餘額: 1000
存 500 後的餘額: 1500

餘額: 1000
取 500 後的餘額: 1000

餘額: 1000
存 500 後的餘額: 1500

餘額: 1000
取 500 後的餘額: 1000

餘額: 1000
存 500 後的餘額: 1500

餘額: 1000
取 500 後的餘額: 1000

餘額: 1000
取 500 後的餘額: 500

餘額: 1000
存 500 後的餘額: 1000

餘額: 1000
取 500 後的餘額: 500

餘額: 1000
存 500 後的餘額: 1000

當前餘額: 1000

可以看到出現瞭混亂,比如第二次1000的餘額取500後還是1000,這種對同一資源的競爭出現瞭競態條件(Race Condition)。

下面來看加鎖的執行結果:

餘額: 1000
取 500 後的餘額: 500

餘額: 500
存 500 後的餘額: 1000

餘額: 1000
取 500 後的餘額: 500

餘額: 500
存 500 後的餘額: 1000

餘額: 1000
取 500 後的餘額: 500

餘額: 500
存 500 後的餘額: 1000

餘額: 1000
存 500 後的餘額: 1500

餘額: 1500
取 500 後的餘額: 1000

餘額: 1000
取 500 後的餘額: 500

餘額: 500
存 500 後的餘額: 1000

當前餘額: 1000

加鎖後就正常瞭。

下面介紹更細化的互斥鎖:讀/寫互斥鎖RWMutex。

二、讀寫鎖RWMutex

1、RWMutex介紹

讀/寫互斥鎖RWMutex包含瞭讀鎖和寫鎖,分別對共享資源的“讀操作”和“寫操作”進行保護。sync.RWMutex類型中的Lock方法和Unlock方法分別用於對寫鎖進行鎖定和解鎖,而它的RLock方法和RUnlock方法則分別用於對讀鎖進行鎖定和解鎖。

有瞭互斥鎖Mutex,為什麼還需要讀寫鎖呢?因為在很多並發操作中,並發讀取占比很大,寫操作相對較少,讀寫鎖可以並發讀取,這樣可以提供服務性能。讀寫鎖具有以下特征:

讀寫鎖 讀鎖 寫鎖
讀鎖 Yes No
寫鎖 No No

也就是說,

  • 如果某個共享資源受到讀鎖和寫鎖保護時,其它goroutine不能進行寫操作。換句話說就是讀寫操作和寫寫操作不能並行執行,也就是讀寫互斥;
  • 受讀鎖保護時,可以同時進行多個讀操作。

在使用讀寫鎖時,還需要註意:

  • 不要對未鎖定的讀寫鎖解鎖;
  • 對讀鎖不能使用寫鎖解鎖
  • 對寫鎖不能使用讀鎖解鎖

2、RWMutex使用實例

改寫前面的取錢和存錢操作,添加查詢餘額的方法:

package main

import (
 "fmt"
 "sync"
)

// account 代表計數器。
type account struct {
 num uint         // 操作次數
 balance int   // 餘額
 rwMu  *sync.RWMutex // 讀寫鎖
}

var sign = make(chan struct{}, 15) //通道,用於等待所有goroutine

// 查看餘額:使用讀鎖
func (c *account) check() {
 defer func() {
        sign <- struct{}{}
    }()
 c.rwMu.RLock()
 defer c.rwMu.RUnlock()
 fmt.Printf("%d 次操作後的餘額: %d\n", c.num, c.balance)
}

// 存錢:寫鎖
func (c *account) deposit(value int) {
 defer func() {
        sign <- struct{}{}
    }()
    c.rwMu.Lock()
 defer c.rwMu.Unlock() 

 fmt.Printf("餘額: %d\n", c.balance)   
 c.num += 1
    c.balance += value
    fmt.Printf("存 %d 後的餘額: %d\n", value, c.balance)
    fmt.Println() 
}

// 取錢:寫鎖
func (c *account) withdraw(value int) {
    defer func() {
        sign <- struct{}{}
    }()
 c.rwMu.Lock()
 defer c.rwMu.Unlock()   
 fmt.Printf("餘額: %d\n", c.balance)     
 c.num += 1
    c.balance -= value
 fmt.Printf("取 %d 後的餘額: %d\n", value, c.balance)
    fmt.Println()  
}


func main() {
 c := account{0, 1000, new(sync.RWMutex)}

 for i:=0; i < 5; i++ {
        go c.withdraw(500) // 取500
        go c.deposit(500)  // 存500
  go c.check()
    }

    for i := 0; i < 15; i++ {
  <-sign
 }
 fmt.Printf("%d 次操作後的餘額: %d\n", c.num, c.balance)

}

執行結果:

餘額: 1000
取 500 後的餘額: 500

1 次操作後的餘額: 500
1 次操作後的餘額: 500
1 次操作後的餘額: 500
1 次操作後的餘額: 500
1 次操作後的餘額: 500
餘額: 500
存 500 後的餘額: 1000

餘額: 1000
取 500 後的餘額: 500

餘額: 500
存 500 後的餘額: 1000

餘額: 1000
存 500 後的餘額: 1500

餘額: 1500
取 500 後的餘額: 1000

餘額: 1000
取 500 後的餘額: 500

餘額: 500
存 500 後的餘額: 1000

餘額: 1000
取 500 後的餘額: 500

餘額: 500
存 500 後的餘額: 1000

10 次操作後的餘額: 1000

讀寫鎖和互斥鎖的不同之處在於讀寫鎖把對共享資源的讀操作和寫操作分開瞭,可以實現更復雜的訪問控制。

總結:

讀寫鎖也是一種互斥鎖,它是互斥鎖的擴展。在使用時需要註意:

  • 加鎖後一定要解鎖
  • 不要重復加鎖或者解鎖
  • 不解鎖未鎖定的鎖
  • 不要傳遞互斥鎖

到此這篇關於Go語言並發編程之互斥鎖Mutex和讀寫鎖RWMutex的文章就介紹到這瞭,更多相關Go語言 Mutex RWMutex內容請搜索WalkonNet以前的文章或繼續瀏覽下面的相關文章希望大傢以後多多支持WalkonNet!

推薦閱讀: