溫馨提示×

溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊×
其他方式登錄
點擊 登錄注冊 即表示同意《億速云用戶服務(wù)條款》

Go語言之并發(fā)資源競爭

發(fā)布時間:2020-07-18 03:50:13 來源:網(wǎng)絡(luò) 閱讀:363 作者:baby神 欄目:編程語言

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


package main

import (
    "fmt"
    "runtime"
    "sync"
)

var (
    count int32
    wg    sync.WaitGroup
)

func main() {
    wg.Add(2)
    go incCount()
    go incCount()
    wg.Wait()
    fmt.Println(count)
}

func incCount() {
    defer wg.Done()
    for i := 0; i < 2; i++ {
        value := count
        runtime.Gosched()
        value++
        count = value
    }
}


這是一個資源競爭的例子。我們可以多運行幾次這個程序,會發(fā)現(xiàn)結(jié)果可能是 2 ,也可以是 3 ,也可能是 4 。因為共享資源count變量沒有任何同步保護,所以兩個goroutine都會對其進行讀寫,會導致對已經(jīng)計算好的結(jié)果覆蓋,以至于產(chǎn)生錯誤結(jié)果。這里我們演示一種可能,兩個goroutine我們暫時稱之為g1和g2。


 g1讀取到count為 0 。


 然后g1暫停了,切換到g2運行,g2讀取到count也為 0 。


 g2暫停,切換到g1,g1對count+1,count變?yōu)?1 。


 g1暫停,切換到g2,g2剛剛已經(jīng)獲取到值 0 ,對其+1,最后賦值給count還是 1 。


 有沒有注意到,剛剛g1對count+1的結(jié)果被g2給覆蓋了,兩個goroutine都+1還是 1 。


不再繼續(xù)演示下去了,到這里結(jié)果已經(jīng)錯了,兩個goroutine相互覆蓋結(jié)果。我們這里的runtime.Gosched()是讓當前goroutine暫停的意思。退回執(zhí)行隊列,讓其他等待的goroutine運行,目的是讓我們演示資源競爭的結(jié)果更明顯。注意,這里還會牽涉到CPU問題,多核會并行,那么資源競爭的效果更明顯。


所以我們對于同一個資源的讀寫必須是原子化的,也就是說,同一時間只能有一個goroutine對共享資源進行讀寫操作。


共享資源競爭的問題,非常復(fù)雜,并且難以察覺,好在Go提供了一個工具來幫助我們檢查,這個就是go build -race命令。我們在當前項目目錄下執(zhí)行這個命令,生成一個可以執(zhí)行文件,然后再運行這個可執(zhí)行文件,就可以看到打印出的檢測信息。


go build -race


多加了一個-race標志,這樣生成的可執(zhí)行程序就自帶了檢測資源競爭的功能。下面我們運行,也是在終端運行。


./hello


我這里示例生成的可執(zhí)行文件名是hello,所以是這么運行的。這時候,我們看終端輸出的檢測結(jié)果。


  hello ./hello       
==================
WARNING: DATA RACE
Read at 0x0000011a5118 by goroutine 7:
  main.incCount()
      /Users/xxx/code/go/src/flysnow.org/hello/main.go:25 +0x76

Previous write at 0x0000011a5118 by goroutine 6:
  main.incCount()
      /Users/xxx/code/go/src/flysnow.org/hello/main.go:28 +0x9a

Goroutine 7 (running) created at:
  main.main()
      /Users/xxx/code/go/src/flysnow.org/hello/main.go:17 +0x77

Goroutine 6 (finished) created at:
  main.main()
      /Users/xxx/code/go/src/flysnow.org/hello/main.go:16 +0x5f
==================
4
Found 1 data race(s)


看,找到一個資源競爭,連在那一行代碼出了問題,都標示出來了。goroutine 7在代碼 25 行讀取共享資源value := count,而這時goroutine 6正在代碼 28 行修改共享資源count = value,而這兩個goroutine都是從main函數(shù)啟動的,在 16、17 行,通過go關(guān)鍵字。


既然我們已經(jīng)知道共享資源競爭的問題,是因為同時有兩個或者多個goroutine對其進行了讀寫,那么我們只要保證,同時只有一個goroutine讀寫不就可以了?,F(xiàn)在我們就看下傳統(tǒng)解決資源競爭的辦法——對資源加鎖。


Go語言提供了atomic包和sync包里的一些函數(shù)對共享資源同步枷鎖,我們先看下atomic包。


package main

import (
    "fmt"
    "runtime"
    "sync"
    "sync/atomic"
)

var (
    count int32
    wg    sync.WaitGroup
)

func main() {
    wg.Add(2)
    go incCount()
    go incCount()
    wg.Wait()
    fmt.Println(count)
}

func incCount() {
    defer wg.Done()
    for i := 0; i < 2; i++ {
        value := atomic.LoadInt32(&count)
        runtime.Gosched()
        value++
        atomic.StoreInt32(&count,value)
    }
}


留意這里atomic.LoadInt32atomic.StoreInt32兩個函數(shù):一個讀取int32類型變量的值,一個是修改int32類型變量的值。這兩個都是原子性的操作,Go已經(jīng)幫助我們在底層使用加鎖機制,保證了共享資源的同步和安全,所以我們可以得到正確的結(jié)果。這時候我們再使用資源競爭檢測工具go build -race檢查,也不會提示有問題了。


atomic包里還有很多原子化的函數(shù)可以保證并發(fā)下資源同步訪問修改的問題。比如函數(shù)atomic.AddInt32可以直接對一個int32類型的變量進行修改,在原值的基礎(chǔ)上再增加多少的功能,也是原子性的。這里不再舉例,大家自己可以試試。


atomic雖然可以解決資源競爭問題,但是比較都是比較簡單的,支持的數(shù)據(jù)類型也有限,所以Go語言還提供了一個sync包。這個sync包里提供了一種互斥型的鎖,可以讓我們自己靈活地控制那些代碼,同時只能有一個goroutine訪問,被sync互斥鎖控制的這段代碼范圍,被稱之為臨界區(qū)。臨界區(qū)的代碼,同一時間,只能又一個goroutine訪問。剛剛那個例子,我們還可以這么改造。


package main

import (
    "fmt"
    "runtime"
    "sync"
)

var (
    count int32
    wg    sync.WaitGroup
    mutex sync.Mutex
)

func main() {
    wg.Add(2)
    go incCount()
    go incCount()
    wg.Wait()
    fmt.Println(count)
}

func incCount() {
    defer wg.Done()
    for i := 0; i < 2; i++ {
        mutex.Lock()
        value := count
        runtime.Gosched()
        value++
        count = value
        mutex.Unlock()
    }
}


實例中,新聲明了一個互斥鎖mutex sync.Mutex。這個互斥鎖有兩個方法,一個是mutex.Lock(),一個是mutex.Unlock()這兩個之間的區(qū)域就是臨界區(qū),臨界區(qū)的代碼是安全的。


示例中我們先調(diào)用mutex.Lock()對有競爭資源的代碼加鎖,這樣當一個goroutine進入這個區(qū)域的時候,其他goroutine就進不來了,只能等待,一直到調(diào)用mutex.Unlock() 釋放這個鎖為止。


這種方式比較靈活,可以讓代碼編寫者任意定義需要保護的代碼范圍,也就是臨界區(qū)。除了原子函數(shù)和互斥鎖,Go還為我們提供了更容易在多個goroutine同步的功能,這就是通道chan,我們會在下次繼續(xù)講解。


向AI問一下細節(jié)

免責聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點不代表本網(wǎng)站立場,如果涉及侵權(quán)請聯(lián)系站長郵箱:is@yisu.com進行舉報,并提供相關(guān)證據(jù),一經(jīng)查實,將立刻刪除涉嫌侵權(quán)內(nèi)容。

AI