每日一抄 Go語言死鎖、活鎖和飢餓概述
阿新 • • 發佈:2022-12-07
DeadLock
package main import ( "fmt" "runtime" "sync" "time" ) /* 死鎖 死鎖是指兩個或兩個以上的程序(或執行緒)在執行過程中,因爭奪資源而造成的一種互相等待的現象,若無外力作用,它們都將無法推進下去。 此時稱系統處於死鎖狀態或系統產生了死鎖,這些永遠在互相等待的程序稱為死鎖程序。 死鎖發生的條件有如下幾種: 1) 互斥條件 執行緒對資源的訪問是排他性的,如果一個執行緒對佔用了某資源,那麼其他執行緒必須處於等待狀態,直到該資源被釋放。 2) 請求和保持條件 執行緒 T1 至少已經保持了一個資源 R1 佔用,但又提出使用另一個資源 R2 請求,而此時,資源 R2 被其他執行緒 T2 佔用,於是該執行緒 T1 也必須等待,但又對自己保持的資源 R1 不釋放。 3) 不剝奪條件 執行緒已獲得的資源,在未使用完之前,不能被其他執行緒剝奪,只能在使用完以後由自己釋放。 4) 環路等待條件 在死鎖發生時,必然存在一個“程序 - 資源環形鏈”,即:{p0,p1,p2,...pn},程序 p0(或執行緒)等待 p1 佔用的資源,p1 等待 p2 佔用的資源,pn 等待 p0 佔用的資源。 最直觀的理解是,p0 等待 p1 佔用的資源,而 p1 而在等待 p0 佔用的資源,於是兩個程序就相互等待。 死鎖解決辦法: 如果併發查詢多個表,約定訪問順序; 在同一個事務中,儘可能做到一次鎖定獲取所需要的資源; 對於容易產生死鎖的業務場景,嘗試升級鎖顆粒度,使用表級鎖; 採用分散式事務鎖或者使用樂觀鎖。 死鎖程式是所有併發程序彼此等待的程式,在這種情況下,如果沒有外界的干預,這個程式將永遠無法恢復。 為了便於大家理解死鎖是什麼,我們先來看一個例子(忽略程式碼中任何不知道的型別,函式,方法或是包,只理解什麼是死鎖即可),程式碼如下所示: */ type value struct { memAccess sync.Mutex value int } func main() { runtime.GOMAXPROCS(3) var wg sync.WaitGroup sum := func(v1, v2 *value) { defer wg.Done() v1.memAccess.Lock() time.Sleep(2 * time.Second) v2.memAccess.Lock() fmt.Printf("sum = %d\n", v1.value+v2.value) v2.memAccess.Unlock() v1.memAccess.Unlock() } product := func(v1, v2 *value) { defer wg.Done() v2.memAccess.Lock() time.Sleep(2 * time.Second) v1.memAccess.Lock() fmt.Printf("sum = %d\n", v1.value*v2.value) v1.memAccess.Unlock() v2.memAccess.Unlock() } var v1, v2 value v1.value = 1 v2.value = 2 wg.Add(2) go sum(&v1, &v2) //加個等待時間就可以了 //time.Sleep(5 * time.Second) go product(&v1, &v2) wg.Wait() fmt.Println("over") }
如果沒有等待時間的話
LiveLock
package main import ( "bytes" "fmt" "runtime" "sync" "sync/atomic" "time" ) /* 活鎖是另一種形式的活躍性問題,該問題儘管不會阻塞執行緒,但也不能繼續執行,因為執行緒將不斷重複同樣的操作,而且總會失敗。 例如執行緒 1 可以使用資源,但它很禮貌,讓其他執行緒先使用資源,執行緒 2 也可以使用資源,但它同樣很紳士,也讓其他執行緒先使用資源。就這樣你讓我,我讓你,最後兩個執行緒都無法使用資源。 活鎖通常發生在處理事務訊息中,如果不能成功處理某個訊息,那麼訊息處理機制將回滾事務,並將它重新放到佇列的開頭。這樣,錯誤的事務被一直回滾重複執行,這種形式的活鎖通常是由過度的錯誤恢復程式碼造成的,因為它錯誤地將不可修復的錯誤認為是可修復的錯誤。 當多個相互協作的執行緒都對彼此進行相應而修改自己的狀態,並使得任何一個執行緒都無法繼續執行時,就導致了活鎖。這就像兩個過於禮貌的人在路上相遇,他們彼此讓路,然後在另一條路上相遇,然後他們就一直這樣避讓下去。 要解決這種活鎖問題,需要在重試機制中引入隨機性。例如在網路上傳送資料包,如果檢測到衝突,都要停止並在一段時間後重發,如果都在 1 秒後重發,還是會衝突,所以引入隨機性可以解決該類問題。 活鎖示例: */ func main() { runtime.GOMAXPROCS(3) cv := sync.NewCond(&sync.Mutex{}) go func() { for range time.Tick(1 * time.Second) { //通過tick控制兩人的步調 cv.Broadcast() } }() takeStep := func() { cv.L.Lock() cv.Wait() cv.L.Unlock() } tryDir := func(dirName string, dir *int32, out *bytes.Buffer) bool { fmt.Fprintf(out, "%+v", dirName) atomic.AddInt32(dir, 1) takeStep() if atomic.LoadInt32(dir) == 1 { //走成功就返回 fmt.Fprintf(out, ".Success") return true } takeStep() //沒走成功,再走回來 atomic.AddInt32(dir, -1) return false } var left, right int32 tryLeft := func(out *bytes.Buffer) bool { return tryDir("向左走", &left, out) } tryRight := func(out *bytes.Buffer) bool { return tryDir("向右走", &right, out) } walk := func(walking *sync.WaitGroup, name string) { var out bytes.Buffer defer walking.Done() defer func() { fmt.Println(out.String()) }() fmt.Fprintf(&out, "%v is trying to scoot:", name) for i := 0; i < 5; i++ { if tryLeft(&out) || tryRight(&out) { return } } fmt.Fprintf(&out, "\n%v is tried!", name) } var trail sync.WaitGroup trail.Add(2) go walk(&trail, "男人") // 男人在路上走 go walk(&trail, "女人") // 女人在路上走 trail.Wait() }
hungery
package main import ( "fmt" "runtime" "sync" "time" ) /* 飢餓是指一個可執行的程序儘管能繼續執行,但被排程器無限期地忽視,而不能被排程執行的情況。 與死鎖不同的是,飢餓鎖在一段時間內,優先順序低的執行緒最終還是會執行的,比如高優先順序的執行緒執行完之後釋放了資源。 活鎖與飢餓是無關的,因為在活鎖中,所有併發程序都是相同的,並且沒有完成工作。更廣泛地說,飢餓通常意味著有一個或多個貪婪的併發程序,它們不公平地阻止一個或多個併發程序,以儘可能有效地完成工作,或者阻止全部併發程序。 下面的示例程式中包含了一個貪婪的 goroutine 和一個平和的 goroutine: */ func main() { runtime.GOMAXPROCS(3) var wg sync.WaitGroup const runtime = 1 * time.Second var sharedLock sync.Mutex greedyWorker := func() { defer wg.Done() var count int for begin := time.Now(); time.Since(begin) <= runtime; { sharedLock.Lock() time.Sleep(3 * time.Nanosecond) sharedLock.Unlock() count++ } fmt.Printf("Greedy worker was able to execute %v work loops\n", count) } politeWorker := func() { defer wg.Done() var count int for begin := time.Now(); time.Since(begin) <= runtime; { sharedLock.Lock() time.Sleep(1 * time.Nanosecond) sharedLock.Unlock() sharedLock.Lock() time.Sleep(1 * time.Nanosecond) sharedLock.Unlock() sharedLock.Lock() time.Sleep(1 * time.Nanosecond) sharedLock.Unlock() count++ } fmt.Printf("Polite worker was able to execute %v work loops\n", count) } wg.Add(2) go greedyWorker() go politeWorker() wg.Wait() } /* 貪婪的 worker 會貪婪地搶佔共享鎖,以完成整個工作迴圈,而平和的 worker 則試圖只在需要時鎖定。兩種 worker 都做同樣多的模擬工作(sleeping 時間為 3ns),可以看到,在同樣的時間裡,貪婪的 worker 工作量幾乎是平和的 worker 工作量的兩倍! 假設兩種 worker 都有同樣大小的臨界區,而不是認為貪婪的 worker 的演算法更有效(或呼叫 Lock 和 Unlock 的時候,它們也不是緩慢的),我們得出這樣的結論,貪婪的 worker 不必要地擴大其持有共享鎖上的臨界區,井阻止(通過飢餓)平和的 worker 的 goroutine 高效工作。 */ /* 總結 不適用鎖肯定會出問題。如果用了,雖然解了前面的問題,但是又出現了更多的新問題。 死鎖:是因為錯誤的使用了鎖,導致異常; 活鎖:是飢餓的一種特殊情況,邏輯上感覺對,程式也一直在正常的跑,但就是效率低,邏輯上進行不下去; 飢餓:與鎖使用的粒度有關,通過計數取樣,可以判斷程序的工作效率。 只要有共享資源的訪問,必定要使其邏輯上進行順序化和原子化,確保訪問一致,這繞不開鎖這個概念。 */