iT邦幫忙

第 12 屆 iThome 鐵人賽

DAY 18
0

這天小櫻的班上又校外教學啦!是採草莓哦!但是要進行下午課程時,園方的資料館突然打不開,導致校外教學即將被中斷。於是小櫻一行人決定向互斥鎖宣戰(吔不是這用詞也太兇)

各位魔法使們能一起幫小助小櫻收服這張 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 時僅限於同一個執行緒,若有若干個執行序去存去 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 writes

goroutine 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 +0x15c

goroutine 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

因為出現錯誤是有機率的,所以如果沒有出現錯誤,可以多試幾次。


中場休息 ── 普化實驗


使用執行緒安全的 Map

如果你曾經是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


後記 (小狼要打開互斥鎖的大門?)

本文大多數圖片來自:


上一篇
#17 Unbuffered Channel & Buffered Channel | Golang魔法使
下一篇
#19 Select & Multiplexing 選擇通道與多路複用 | Golang魔法使
系列文
Golang魔法使 ─ 30天從零開始學習Go語言 | 比Python還簡單 | 理科生一定學得會 | 文科生不好說30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言