1. 程式人生 > 其它 >每日一抄 Go語言死鎖、活鎖和飢餓概述

每日一抄 Go語言死鎖、活鎖和飢餓概述

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 高效工作。
*/

/*
總結
不適用鎖肯定會出問題。如果用了,雖然解了前面的問題,但是又出現了更多的新問題。
死鎖:是因為錯誤的使用了鎖,導致異常;
活鎖:是飢餓的一種特殊情況,邏輯上感覺對,程式也一直在正常的跑,但就是效率低,邏輯上進行不下去;
飢餓:與鎖使用的粒度有關,通過計數取樣,可以判斷程序的工作效率。

只要有共享資源的訪問,必定要使其邏輯上進行順序化和原子化,確保訪問一致,這繞不開鎖這個概念。
 */