Golang併發之共享記憶體變數

張君鴻發表於2019-03-28

應該說,無論使用哪一種程式語言開發應用程式,併發程式設計都是複雜的,而Go語言內建的併發支援,則讓Go併發程式設計變得很簡單。

CSP

CSP,即順序通訊程式,是Go語言中原生支援的併發模型,一般使用goroutine和channel來實現,CSP的程式設計思想是“通過通訊共享記憶體,而不是通過共享記憶體來通訊”,因此使用CSP思想來開發併發程式,一般是使用channel串聯多個goroutine,最終達到多個goroutine順序執行的目的。

共享記憶體變數

我們知道,單個的goutine程式碼是順序執行,而併發程式設計時,建立多個goroutine,但我們並不能確定不同的goroutine之間的執行順序,多個goroutine之間大部分情況是程式碼交叉執行,在執行過程中,可能會修改或讀取共享記憶體變數,這樣就會產生資料競爭,發生一些意外之外的結果。

package main

var balance int
//存款
func Deposit(amount int) { 
    balance = balance + amount 
}
//讀取餘額
func Balance() int {
    return balance
}

func main(){
    //小王:存600,並讀取餘額
    go func(){
        Deposit(600)
        fmt.Println(Balance())
    }()
    //小張:存500
    go func(){
        Deposit(500)
    }()
    
    time.Sleep(time.Second)
    fmt.Println(balance)
}
複製程式碼

上面的例子叫銀行存款問題,是演示併發的經典例子。

一般我們認為,這個例子的執行結果只有三種:

  1. 小王存600,小王讀取餘額為600,小張再存500,總金額為1100
  2. 小張存500,小王存600,小王讀取餘額為500,總金額為1100
  3. 小王存600,小張存500,小王讀取餘額為500,總金額為1100

上面的情況,都是假設存款操作是順序的,但是,還存在一種情況,也就是小王或小張併發執行存款操作,這時候會發生存款金額丟失的風險。

看到上面的例子之後,我們知道資料競爭會產生嚴重的後果,那如何避免資料競爭呢?有三種:

  1. 通過channel串聯goroutine,達到順序執行的效果,避免競爭。
  2. 不在併發程式中修改共享變數,這當然是不太可能的情況。
  3. 通過使用鎖,使用同一時間只有一個goroutine可以修改記憶體中的變數,也就使用不同goroutine修改變數時發生互斥行為。

sync.Mutex:互斥鎖

我們可以使用Go語言提供的互斥鎖來避免上述的資料競爭行為的發生,可以把程式碼進行相應的修改:

mu sync.Mutex // 宣告一個互斥鎖
func Deposit(amount int) {
    mu.Lock()//獲取鎖
    balance = balance + amount
    mu.Unlock()//釋放鎖
}
//讀取餘額
func Balance() int {
    mu.Lock()//獲取鎖
    return balance
    mu.Unlock()//釋放鎖
}
複製程式碼

sync.RWMutex:讀寫鎖

當我們使用Mutex互斥鎖的時候,那麼無論是讀取還是修改,都需要等待其他goroutine釋放鎖,但是讀取相對修改來是,是安全的操作,Go提供了另外一種鎖,sync.RWMutex,讀寫鎖,這種鎖,多個讀取的時候,不會鎖,只有修改時候,需要等到所有讀取的鎖釋放,才能修改,所以我們可以把Balance()函式修改為:

rmu sync.RWMutex
func Balance() int {
    rmu.RLock()//獲取讀鎖
    return balance
    rmu.RUnlock()//釋放讀鎖
}
複製程式碼

更好地使用鎖

上面的例子中,我們都是在函式後面釋放鎖的,但實際開發中,函式的程式碼很長,有各種判斷,我們無法保證函式能執行到最後,併成功釋放鎖,如果中發生錯誤,無法釋放鎖,就造成其他goroutine的阻塞,因此可以使用defer關鍵字,讓函式無論如何都會釋放鎖。

package main
import "sync"

var balance int
mu sync.Mutex // 宣告一個互斥鎖
rmu sync.RWMutex
//存款
func Deposit(amount int) {
    mu.Lock()//獲取鎖
    balance = balance + amount
    mu.Unlock()//釋放鎖
}
//讀取餘額
func Balance() int {
    rmu.RLock()//獲取讀鎖
    return balance
    rmu.RUnlock()//釋放讀鎖
}

func main(){
    //小王:存600,並讀取餘額
    go func(){
        Deposit(600)
        fmt.Println(Balance())
    }()
    //小張:存500
    go func(){
        Deposit(500)
    }()
    
    time.Sleep(time.Second)
    fmt.Println(balance)
}
複製程式碼

總結

當然,在實際專案併發程式設計的時候,我們遇到的情況要遠比上述例子複雜得多,因此還要多多練習,讓自己對併發有更學層次的理解。

相關文章