這天小櫻的班上又校外教學啦!是採草莓哦!但是要進行下午課程時,園方的資料館突然打不開,導致校外教學即將被中斷。於是小櫻一行人決定向互斥鎖宣戰(吔不是這用詞也太兇)
各位魔法使們能一起幫小助小櫻收服這張 Mutex 牌嗎?
在中文中,同步有同時的意思,比如我們一起同步做某件事。但是在魔法科學(資訊科學)中,同步的意思是:「在同一個時間軸上」也就是說 A 執行完了再換 B 執行,B 執行完了再換 C 執行。如果 A 執行時,B 卻沒有乖乖等 A 執行,而偷跑,那麼這時就稱為異步。
基本上,在不使用 goroutine 所設計出的程式都是同步的,那麼為什麼要有異步呢?因為電腦在處理某些指令時會比較慢,如果該步驟不影響後續程式進行,那麼就可以使用異步,讓該指令不影響主程序進行。比如讀取硬碟內容、讀取網路上的內容這些都會比較慢一些,另開一條執行緒去讀取可以不影響主執行緒的內容。
試想一個情況,今天小櫻為了成為魔法使,離開友枝市學習獨立,但人在他鄉的小櫻錢漸漸不夠了,只好打電話跟哥哥桃矢要錢。就在桃矢匯款時,小櫻也準備取款,如果這時好巧不巧的兩人動作重疊了這時會發生什麼問題呢?
package main
import "fmt"
var balance int
func withdrawal(){ //提款
balance = balance - 100
}
func deposit(done chan bool){ //存款
balance = balance + 100
done <- true
}
func main(){
done := make(chan bool)
for i:=0; i < 1000000; i = i+1{
balance = 100
go deposit(done)
withdrawal()
<-done
if balance != 100 {
fmt.Println("error!", balance)
}
}
}
執行結果:
error! 0
error! 200
(略, 每次執行都稍微有差異)
因為並不是每一次運氣都那麼差所以我們連續實測 100 萬次,實測結果在錯誤發生時不是 0 或 200
要怎麼避免這個問題呢?其實就是要讓 withdrawal() 和 deposit() 同步,一個提完錢另一個才能存錢,或者一個存完錢另一個才能提,存錢和提錢的順序不重要,重要的是不可以異步進行。
為了避免兩個執行緒同時讀寫同一個變數,我們可以使用 Golang 提供的 sync package
將變數狀態鎖住。
我們需要使用 sync package
中的「互斥鎖」(Mutex)。斥鎖的特性可以用來將需要「同步」執行的地方「上鎖」(lock),而執行完後再進行「解鎖」(unlock)。如果有其他執行緒想要進入上鎖的區域,就必需等待解鎖,如此一來就可以避免以異步的方式處理應該同步的區域
至於實作上是利用 new() 來宣告一個的互斥鎖 *sync.Mutex,利用 *sync.Mutex
的兩個方法 Lock()
及 Unlock()
分別控制「上鎖」和「解鎖」
package main
import (
"fmt"
"sync"
)
var balance int
func withdrawal(lock *sync.Mutex){ //提款
lock.Lock() // 上鎖
balance = balance - 100
lock.Unlock() // 解鎖
}
func deposit(done chan bool, lock *sync.Mutex){ //存款
lock.Lock() // 上住
balance = balance + 100
lock.Unlock() // 解鎖
done <- true
}
func main(){
done := make(chan bool)
lock := new(sync.Mutex) // lock 型態:*sync.Mutex
for i:=0; i < 1000000; i = i+1{
balance = 100
go deposit(done, lock)
withdrawal(lock)
<-done
if balance != 100 {
fmt.Println("error!", balance)
}
}
}
執行結果:
(什麼都沒有)
該範例僅將「互斥鎖」使用於「 2個執行緒」控制 balance
時,然而實際使用時,並不侷限在 2 個執行緒,即使有多個執行緒也可以使用
所謂的「執行緒安全」就是指:某個函數、函數庫在多執行緒環境中被調用時,能夠正確地處理多個執行緒之間的共享變數。
在先前的課程中有提到 Map 在實作上相當複雜,可以說是黑魔法。然而,存取 Map 時僅限於同一個執行緒,若有若干個執行序去存去 Map 則有機率會出錯!
package main
import "fmt"
func test(){
m := make(map[string]int)
done := make(chan bool, 6)
go func(){
m["小狼"] = 0
done <- true
}()
go func(){
m["小櫻"] = 1
done <- true
}()
go func(){
m["知世"] = 2
done <- true
}()
go func(){
m["小可"] = 3
done <- true
}()
go func(){
m["桃矢"] = 4
done <- true
}()
go func(){
m["歌帆"] = 5
done <- true
}()
// 等待執行緒結束
for i := 0; i < 6; i = i + 1{
<- done
}
fmt.Println(m)
}
func main(){
for i := 0; i < 10; i = i+1{
test()
}
}
執行結果:
map[小可:3 小櫻:1 小狼:0 桃矢:4 歌帆:5 知世:2]
fatal error: concurrent map writesgoroutine 16 [running]:
runtime.throw(0x4c78d0, 0x15)
C:/Go/src/runtime/panic.go:617 +0x79 fp=0xc00009ff30 sp=0xc00009ff00 pc=0x42b0b9
runtime.mapassign_faststr(0x4a9d80, 0xc00006e360, 0x4c4af6, 0x6, 0x0)
C:/Go/src/runtime/map_faststr.go:211 +0x431 fp=0xc00009ff98 sp=0xc00009ff30 pc=0x410541
main.test.func6(0xc00006e360, 0xc000018150)
C:/Users/liao2/OneDrive/go-tutorial/lesson18c.go:34 +0x53 fp=0xc00009ffd0 sp=0xc00009ff98 pc=0x491693
runtime.goexit()
C:/Go/src/runtime/asm_amd64.s:1337 +0x1 fp=0xc00009ffd8 sp=0xc00009ffd0 pc=0x451d21
created by main.test
C:/Users/liao2/OneDrive/go-tutorial/lesson18c.go:33 +0x15cgoroutine 1 [chan receive]:
main.test()
C:/Users/liao2/OneDrive/go-tutorial/lesson18c.go:40 +0x17c
main.main()
C:/Users/liao2/OneDrive/go-tutorial/lesson18c.go:48 +0x31
exit status 2
因為出現錯誤是有機率的,所以如果沒有出現錯誤,可以多試幾次。
如果你曾經是java魔法使,應該可以理解這就像 hashmap 跟 hashtable 的差異,在使用同一個執行序時搭配 hashmap 使用有較好的效能體驗,然後因為 hashmap 為執行序不安全,所以如果要跨執行序使用,則會選擇 hashtable
package main
import(
"fmt"
"sync"
)
func test(){
m := new(sync.Map)
done := make(chan bool, 6)
go func(){
m.LoadOrStore("小狼", 0)
done <- true
}()
go func(){
m.LoadOrStore("小櫻", 1)
done <- true
}()
go func(){
m.LoadOrStore("知世", 2)
done <- true
}()
go func(){
m.LoadOrStore("小可", 3)
done <- true
}()
go func(){
m.LoadOrStore("桃矢", 4)
done <- true
}()
go func(){
m.LoadOrStore("歌帆", 5)
done <- true
}()
// 等待執行緒結束
for i := 0; i < 6; i = i + 1{
<- done
}
fmt.Print("map[")
m.Range(func(key, value interface{}) bool{
fmt.Printf("%s:%d ",key, value)
return true
})
fmt.Println("]")
}
func main(){
for i := 0; i < 10; i = i+1{
test()
}
}
執行結果:
map[歌帆:5 小狼:0 小櫻:1 知世:2 小可:3 桃矢:4 ]
map[小櫻:1 知世:2 小可:3 桃矢:4 小狼:0 歌帆:5 ]
map[小狼:0 小櫻:1 知世:2 小可:3 桃矢:4 歌帆:5 ]
map[歌帆:5 小狼:0 小櫻:1 知世:2 小可:3 桃矢:4 ]
map[小可:3 桃矢:4 歌帆:5 小狼:0 小櫻:1 知世:2 ]
map[歌帆:5 小狼:0 小櫻:1 知世:2 小可:3 桃矢:4 ]
map[小可:3 桃矢:4 歌帆:5 小狼:0 小櫻:1 知世:2 ]
map[知世:2 小可:3 桃矢:4 歌帆:5 小狼:0 小櫻:1 ]
map[歌帆:5 小狼:0 小櫻:1 知世:2 小可:3 桃矢:4 ]
map[歌帆:5 小狼:0 小櫻:1 桃矢:4 知世:2 小可:3 ]
每次執行結果都稍有差異
相關的使用方法可以參考 sync - The Go Programming Language
本文大多數圖片來自: