GO語言臨界資源安全問題的深入理解

一、臨界資源

臨界資源: 指並發環境中多個進程/線程/協程共享的資源。

但是在並發編程中對臨界資源的處理不當, 往往會導致數據不一致的問題。

示例代碼:

package main
​
import (
    "fmt"
    "time"
)
​
func main()  {
    a := 1
    go func() {
        a = 2
        fmt.Println("子goroutine。。",a)
    }()
    a = 3
    time.Sleep(1)
    fmt.Println("main goroutine。。",a)
}

我們通過終端命令來執行:

能夠發現一處被多個goroutine共享的數據。

二、臨界資源安全問題

並發本身並不復雜,但是因為有瞭資源競爭的問題,就使得我們開發出好的並發程序變得復雜起來,因為會引起很多莫名其妙的問題。

如果多個goroutine在訪問同一個數據資源的時候,其中一個線程修改瞭數據,那麼這個數值就被修改瞭,對於其他的goroutine來講,這個數值可能是不對的。

舉個例子,我們通過並發來實現火車站售票這個程序。一共有100張票,4個售票口同時出售。

我們先來看一下示例代碼:

package main
​
import (
    "fmt"
    "math/rand"
    "time"
)
​
//全局變量
var ticket = 10 // 100張票
​
func main() {
    /*
    4個goroutine,模擬4個售票口,4個子程序操作同一個共享數據。
     */
    go saleTickets("售票口1") // g1,100
    go saleTickets("售票口2") // g2,100
    go saleTickets("售票口3") //g3,100
    go saleTickets("售票口4") //g4,100
​
    time.Sleep(5*time.Second)
}
​
func saleTickets(name string) {
    rand.Seed(time.Now().UnixNano())
    //for i:=1;i<=100;i++{
    //  fmt.Println(name,"售出:",i)
    //}
    for { //ticket=1
        if ticket > 0 { //g1,g3,g2,g4
            //睡眠
            time.Sleep(time.Duration(rand.Intn(1000)) * time.Millisecond)
            // g1 ,g3, g2,g4
            fmt.Println(name, "售出:", ticket)  // 1 , 0, -1 , -2
            ticket--   //0 , -1 ,-2 , -3
        } else {
            fmt.Println(name,"售罄,沒有票瞭。。")
            break
        }
    }
}
​

我們為瞭更好的觀察臨界資源問題,每個goroutine先睡眠一個隨機數,然後再售票,我們發現程序的運行結果,還可以賣出編號為負數的票。

分析:

我們的賣票邏輯是先判斷票數的編號是否為負數,如果大於0,然後我們就進行賣票,隻不過在賣票錢先睡眠,然後再賣,假如說此時已經賣票到隻剩最後1張瞭,某一個goroutine持有瞭CPU的時間片,那麼它再片段是否有票的時候,條件是成立的,所以它可以賣票編號為1的最後一張票。但是因為它在賣之前,先睡眠瞭,那麼其他的goroutine就會持有CPU的時間片,而此時這張票還沒有被賣出,那麼第二個goroutine再判斷是否有票的時候,條件也是成立的,那麼它可以賣出這張票,然而它也進入瞭睡眠。。其他的第三個第四個goroutine都是這樣的邏輯,當某個goroutine醒來的時候,不會再判斷是否有票,而是直接售出,這樣就賣出最後一張票瞭,然而其他的goroutine醒來的時候,就會陸續賣出瞭第0張,-1張,-2張。

這就是臨界資源的不安全問題。某一個goroutine在訪問某個數據資源的時候,按照數值,已經判斷好瞭條件,然後又被其他的goroutine搶占瞭資源,並修改瞭數值,等這個goroutine再繼續訪問這個數據的時候,數值已經不對瞭。

三、臨界資源安全問題的解決

要想解決臨界資源安全的問題,很多編程語言的解決方案都是同步。通過上鎖的方式,某一時間段,隻能允許一個goroutine來訪問這個共享數據,當前goroutine訪問完畢,解鎖後,其他的goroutine才能來訪問。

我們可以借助於sync包下的鎖操作。

示例代碼:

package main
​
import (
    "fmt"
    "math/rand"
    "time"
    "sync"
)
​
//全局變量
var ticket = 10 // 100張票
​
var wg sync.WaitGroup
var matex sync.Mutex // 創建鎖頭
​
func main() {
    /*
    4個goroutine,模擬4個售票口,4個子程序操作同一個共享數據。
     */
    wg.Add(4)
    go saleTickets("售票口1") // g1,100
    go saleTickets("售票口2") // g2,100
    go saleTickets("售票口3") //g3,100
    go saleTickets("售票口4") //g4,100
    wg.Wait()              // main要等待。。。
​
    //time.Sleep(5*time.Second)
}
​
func saleTickets(name string) {
    rand.Seed(time.Now().UnixNano())
    defer wg.Done()
    //for i:=1;i<=100;i++{
    //  fmt.Println(name,"售出:",i)
    //}
    for { //ticket=1
        matex.Lock()
        if ticket > 0 { //g1,g3,g2,g4
            //睡眠
            time.Sleep(time.Duration(rand.Intn(1000)) * time.Millisecond)
            // g1 ,g3, g2,g4
            fmt.Println(name, "售出:", ticket) // 1 , 0, -1 , -2
            ticket--                         //0 , -1 ,-2 , -3
        } else {
            matex.Unlock() //解鎖
            fmt.Println(name, "售罄,沒有票瞭。。")
            break
        }
        matex.Unlock() //解鎖
    }
}
​

運行結果:

四、寫在最後

在Go的並發編程中有一句很經典的話:不要以共享內存的方式去通信,而要以通信的方式去共享內存。

在Go語言中並不鼓勵用鎖保護共享狀態的方式在不同的Goroutine中分享信息(以共享內存的方式去通信)。而是鼓勵通過channel將共享狀態或共享狀態的變化在各個Goroutine之間傳遞(以通信的方式去共享內存),這樣同樣能像用鎖一樣保證在同一的時間隻有一個Goroutine訪問共享狀態。

當然,在主流的編程語言中為瞭保證多線程之間共享數據安全性和一致性,都會提供一套基本的同步工具集,如鎖,條件變量,原子操作等等。Go語言標準庫也毫不意外的提供瞭這些同步機制,使用方式也和其他語言也差不多。

到此這篇關於GO語言臨界資源安全問題的深入理解的文章就介紹到這瞭,更多相關GO語言臨界資源安全 內容請搜索WalkonNet以前的文章或繼續瀏覽下面的相關文章希望大傢以後多多支持WalkonNet!

推薦閱讀: