新聞中心
本節(jié)我們來介紹一下死鎖、活鎖和饑餓這三個概念。

死鎖
死鎖是指兩個或兩個以上的進程(或線程)在執(zhí)行過程中,因爭奪資源而造成的一種互相等待的現(xiàn)象,若無外力作用,它們都將無法推進下去。此時稱系統(tǒng)處于死鎖狀態(tài)或系統(tǒng)產(chǎn)生了死鎖,這些永遠在互相等待的進程稱為死鎖進程。
死鎖發(fā)生的條件有如下幾種:
1) 互斥條件
線程對資源的訪問是排他性的,如果一個線程對占用了某資源,那么其他線程必須處于等待狀態(tài),直到該資源被釋放。
2) 請求和保持條件
線程 T1 至少已經(jīng)保持了一個資源 R1 占用,但又提出使用另一個資源 R2 請求,而此時,資源 R2 被其他線程 T2 占用,于是該線程 T1 也必須等待,但又對自己保持的資源 R1 不釋放。
3) 不剝奪條件
線程已獲得的資源,在未使用完之前,不能被其他線程剝奪,只能在使用完以后由自己釋放。
4) 環(huán)路等待條件
在死鎖發(fā)生時,必然存在一個“進程 - 資源環(huán)形鏈”,即:{p0,p1,p2,...pn},進程 p0(或線程)等待 p1 占用的資源,p1 等待 p2 占用的資源,pn 等待 p0 占用的資源。
最直觀的理解是,p0 等待 p1 占用的資源,而 p1 而在等待 p0 占用的資源,于是兩個進程就相互等待。
死鎖解決辦法:
- 如果并發(fā)查詢多個表,約定訪問順序;
- 在同一個事務(wù)中,盡可能做到一次鎖定獲取所需要的資源;
- 對于容易產(chǎn)生死鎖的業(yè)務(wù)場景,嘗試升級鎖顆粒度,使用表級鎖;
- 采用分布式事務(wù)鎖或者使用樂觀鎖。
死鎖程序是所有并發(fā)進程彼此等待的程序,在這種情況下,如果沒有外界的干預(yù),這個程序?qū)⒂肋h無法恢復(fù)。
為了便于大家理解死鎖是什么,我們先來看一個例子(忽略代碼中任何不知道的類型,函數(shù),方法或是包,只理解什么是死鎖即可),代碼如下所示:
package main
import (
"fmt"
"runtime"
"sync"
"time"
)
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("product = %d\n", v1.value*v2.value)
v1.memAccess.Unlock()
v2.memAccess.Unlock()
}
var v1, v2 value
v1.value = 1
v2.value = 1
wg.Add(2)
go sum(&v1, &v2)
go product(&v1, &v2)
wg.Wait()
} 運行上面的代碼,可能會看到:
fatal error: all goroutines are asleep - deadlock!
為什么呢?如果仔細觀察,就可以在此代碼中看到時機問題,以下是運行時的圖形表示。
圖 :一個因時間問題導致死鎖的演示
活鎖
活鎖是另一種形式的活躍性問題,該問題盡管不會阻塞線程,但也不能繼續(xù)執(zhí)行,因為線程將不斷重復(fù)同樣的操作,而且總會失敗。
例如線程 1 可以使用資源,但它很禮貌,讓其他線程先使用資源,線程 2 也可以使用資源,但它同樣很紳士,也讓其他線程先使用資源。就這樣你讓我,我讓你,最后兩個線程都無法使用資源。
活鎖通常發(fā)生在處理事務(wù)消息中,如果不能成功處理某個消息,那么消息處理機制將回滾事務(wù),并將它重新放到隊列的開頭。這樣,錯誤的事務(wù)被一直回滾重復(fù)執(zhí)行,這種形式的活鎖通常是由過度的錯誤恢復(fù)代碼造成的,因為它錯誤地將不可修復(fù)的錯誤認為是可修復(fù)的錯誤。
當多個相互協(xié)作的線程都對彼此進行相應(yīng)而修改自己的狀態(tài),并使得任何一個線程都無法繼續(xù)執(zhí)行時,就導致了活鎖。這就像兩個過于禮貌的人在路上相遇,他們彼此讓路,然后在另一條路上相遇,然后他們就一直這樣避讓下去。
要解決這種活鎖問題,需要在重試機制中引入隨機性。例如在網(wǎng)絡(luò)上發(fā)送數(shù)據(jù)包,如果檢測到?jīng)_突,都要停止并在一段時間后重發(fā),如果都在 1 秒后重發(fā),還是會沖突,所以引入隨機性可以解決該類問題。
下面通過示例來演示一下活鎖:
package main
import (
"bytes"
"fmt"
"runtime"
"sync"
"sync/atomic"
"time"
)
func main() {
runtime.GOMAXPROCS(3)
cv := sync.NewCond(&sync.Mutex{})
go func() {
for range time.Tick(1 * time.Second) { // 通過tick控制兩個人的步調(diào)
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.Fprint(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()
} 輸出結(jié)果如下:
go run main.go
女人 is trying to scoot: 向左走 向右走 向左走 向右走 向左走 向右走 向左走 向右走 向左走 向右走
女人 is tried!
男人 is trying to scoot: 向左走 向右走 向左走 向右走 向左走 向右走 向左走 向右走 向左走 向右走
男人 is tried!
這個例子演示了使用活鎖的一個十分常見的原因,兩個或兩個以上的并發(fā)進程試圖在沒有協(xié)調(diào)的情況下防止死鎖。這就好比,如果走廊里的人都同意,只有一個人會移動,那就不會有活鎖;一個人會站著不動,另一個人會移到另一邊,他們就會繼續(xù)移動。
活鎖和死鎖的區(qū)別在于,處于活鎖的實體是在不斷的改變狀態(tài),所謂的“活”,而處于死鎖的實體表現(xiàn)為等待,活鎖有可能自行解開,死鎖則不能。
饑餓
饑餓是指一個可運行的進程盡管能繼續(xù)執(zhí)行,但被調(diào)度器無限期地忽視,而不能被調(diào)度執(zhí)行的情況。
與死鎖不同的是,饑餓鎖在一段時間內(nèi),優(yōu)先級低的線程最終還是會執(zhí)行的,比如高優(yōu)先級的線程執(zhí)行完之后釋放了資源。
活鎖與饑餓是無關(guān)的,因為在活鎖中,所有并發(fā)進程都是相同的,并且沒有完成工作。更廣泛地說,饑餓通常意味著有一個或多個貪婪的并發(fā)進程,它們不公平地阻止一個或多個并發(fā)進程,以盡可能有效地完成工作,或者阻止全部并發(fā)進程。
下面的示例程序中包含了一個貪婪的 goroutine 和一個平和的 goroutine:
package main
import (
"fmt"
"runtime"
"sync"
"time"
)
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()
}輸出如下:
Greedy worker was able to execute 276 work loops
Polite worker was able to execute 92 work loops
貪婪的 worker 會貪婪地搶占共享鎖,以完成整個工作循環(huán),而平和的 worker 則試圖只在需要時鎖定。兩種 worker 都做同樣多的模擬工作(sleeping 時間為 3ns),可以看到,在同樣的時間里,貪婪的 worker 工作量幾乎是平和的 worker 工作量的兩倍!
假設(shè)兩種 worker 都有同樣大小的臨界區(qū),而不是認為貪婪的 worker 的算法更有效(或調(diào)用 Lock 和 Unlock 的時候,它們也不是緩慢的),我們得出這樣的結(jié)論,貪婪的 worker 不必要地擴大其持有共享鎖上的臨界區(qū),井阻止(通過饑餓)平和的 worker 的 goroutine 高效工作。
總結(jié)
不適用鎖肯定會出問題。如果用了,雖然解了前面的問題,但是又出現(xiàn)了更多的新問題。
- 死鎖:是因為錯誤的使用了鎖,導致異常;
- 活鎖:是饑餓的一種特殊情況,邏輯上感覺對,程序也一直在正常的跑,但就是效率低,邏輯上進行不下去;
- 饑餓:與鎖使用的粒度有關(guān),通過計數(shù)取樣,可以判斷進程的工作效率。
只要有共享資源的訪問,必定要使其邏輯上進行順序化和原子化,確保訪問一致,這繞不開鎖這個概念。
文章題目:創(chuàng)新互聯(lián)GO教程:Go語言死鎖、活鎖和饑餓概述
當前網(wǎng)址:http://m.fisionsoft.com.cn/article/cdgeiio.html


咨詢
建站咨詢
