應該說,無論使用哪一種程式語言開發應用程式,併發程式設計都是複雜的,而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)
}
複製程式碼
上面的例子叫銀行存款問題,是演示併發的經典例子。
一般我們認為,這個例子的執行結果只有三種:
- 小王存600,小王讀取餘額為600,小張再存500,總金額為1100
- 小張存500,小王存600,小王讀取餘額為500,總金額為1100
- 小王存600,小張存500,小王讀取餘額為500,總金額為1100
上面的情況,都是假設存款操作是順序的,但是,還存在一種情況,也就是小王或小張併發執行存款操作,這時候會發生存款金額丟失的風險。
鎖
看到上面的例子之後,我們知道資料競爭會產生嚴重的後果,那如何避免資料競爭呢?有三種:
- 通過channel串聯goroutine,達到順序執行的效果,避免競爭。
- 不在併發程式中修改共享變數,這當然是不太可能的情況。
- 通過使用鎖,使用同一時間只有一個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)
}
複製程式碼
總結
當然,在實際專案併發程式設計的時候,我們遇到的情況要遠比上述例子複雜得多,因此還要多多練習,讓自己對併發有更學層次的理解。