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

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


咨詢
建站咨詢
