日韩无码专区无码一级三级片|91人人爱网站中日韩无码电影|厨房大战丰满熟妇|AV高清无码在线免费观看|另类AV日韩少妇熟女|中文日本大黄一级黄色片|色情在线视频免费|亚洲成人特黄a片|黄片wwwav色图欧美|欧亚乱色一区二区三区

RELATEED CONSULTING
相關(guān)咨詢
選擇下列產(chǎn)品馬上在線溝通
服務(wù)時間:8:30-17:00
你可能遇到了下面的問題
關(guān)閉右側(cè)工具欄

新聞中心

這里有您想知道的互聯(lián)網(wǎng)營銷解決方案
Golang Channel 三大坑,你踩過了嘛?

1. 前言

本文來梳理一下使用 channel 中常見的三大坑:panic、死鎖、內(nèi)存泄漏,做到防患于未然。

峽江網(wǎng)站建設(shè)公司成都創(chuàng)新互聯(lián)公司,峽江網(wǎng)站設(shè)計制作,有大型網(wǎng)站制作公司豐富經(jīng)驗。已為峽江1000多家提供企業(yè)網(wǎng)站建設(shè)服務(wù)。企業(yè)網(wǎng)站搭建\外貿(mào)網(wǎng)站建設(shè)要多少錢,請找那個售后服務(wù)好的峽江做網(wǎng)站的公司定做!

2. 死鎖

go 語言新手在編譯時很容易碰到這個死鎖的問題:

fatal error: all goroutines are asleep - deadlock!

這個就是喜聞樂見的「死鎖」了…… 在操作系統(tǒng)中,我們學(xué)過,「死鎖」就是兩個線程互相等待,耗在那里,最后程序不得不終止。go 語言中的「死鎖」也是類似的,兩個 goroutine 互相等待,導(dǎo)致程序耗在那里,無法繼續(xù)跑下去??戳撕芏嗨梨i的案例后,channel 導(dǎo)致的死鎖可以歸納為以下幾類案例(先討論 unbuffered channel 的情況):

2.1 只有生產(chǎn)者,沒有消費者,或者反過來

channel 的生產(chǎn)者和消費者必須成對出現(xiàn),如果缺乏一個,就會造成死鎖,例如:

// 只有生產(chǎn)者,沒有消費者
func f1() {
ch := make(chan int)
ch <- 1
}

或是:

// 只有消費者,沒有生產(chǎn)者
func f2() {
ch := make(chan int)
<-ch
}

2.2 生產(chǎn)者和消費者出現(xiàn)在同一個 goroutine 中

除了需要成對出現(xiàn),還需要出現(xiàn)在不同的 goroutine 中,例如:

// 同一個 goroutine 中同時出現(xiàn)生產(chǎn)者和消費者
func f3() {
ch := make(chan int)
ch <- 1 // 由于消費者還沒執(zhí)行到,這里會一直阻塞住
<-ch
}

對于 buffered channel 則是:

2.3 buffered channel 已滿,且出現(xiàn)上述情況

buffered channel 會將收到的元素先存在 hchan 結(jié)構(gòu)體的 ringbuffer 中,繼而才會發(fā)生阻塞。而當(dāng)發(fā)生阻塞時,如果阻塞了主 goroutine ,則也會出現(xiàn)死鎖。

所以實際使用中,推薦盡量使用 buffered channel ,使用起來會更安全,在下文的「內(nèi)存泄漏」相關(guān)內(nèi)容也會提及。

3. 內(nèi)存泄漏

內(nèi)存泄漏一般都是通過 OOM(Out of Memory) 告警或者發(fā)布過程中對內(nèi)存的觀察發(fā)現(xiàn)的,服務(wù)內(nèi)存往往都是緩慢上升,直到被系統(tǒng) OOM 掉清空內(nèi)存再周而復(fù)始。

在 go 語言中,錯誤地使用 channel 會導(dǎo)致 goroutine 泄漏,進而導(dǎo)致內(nèi)存泄漏。

3.1 如何實現(xiàn) goroutine 泄漏呢?

不會修 bug,我還不會寫 bug 嗎?讓 goroutine 泄漏的核心就是:

生產(chǎn)者/消費者 所在的 goroutine 已經(jīng)退出,而其對應(yīng)的 消費者/生產(chǎn)者 所在的 goroutine 會永遠(yuǎn)阻塞住,直到進程退出。

3.2 生產(chǎn)者阻塞導(dǎo)致泄漏

我們一般會用 channel 來做一些超時控制,例如下面這個例子:

func leak1() {
ch := make(chan int)
// g1
go func() {
time.Sleep(2 * time.Second) // 模擬 io 操作
ch <- 100 // 模擬返回結(jié)果
}()

// g2
// 阻塞住,直到超時或返回
select {
case <-time.After(500 * time.Millisecond):
fmt.Println("timeout! exit...")
case result := <-ch:
fmt.Printf("result: %d\n", result)
}
}

這里我們用 goroutine g1 來模擬 io 操作,主 goroutine g2 來模擬客戶端的處理邏輯。

  • 假設(shè)客戶端超時為 500ms,而實際請求耗時為 2s,則 select 會走到 timeout 的邏輯,這時g2? 退出,channelch 沒有消費者,會一直在等待狀態(tài),輸出如下:
Goroutine num: 1
timeout! exit...
Goroutine num: 2

如果這是在 server 代碼中,這個請求處理完后,g1 就會掛起、發(fā)生泄漏了,就等著 OOM 吧 =。=。

  • 假設(shè)客戶端超時調(diào)整為 5000ms,實際請求耗時 2s,則 select 會進入獲取 result 的分支,輸出如下:
Goroutine num: 1
timeout! exit...
Goroutine num: 2

3.3 消費者阻塞導(dǎo)致泄漏

如果生產(chǎn)者不繼續(xù)生產(chǎn),消費者所在的 goroutine 也會阻塞住,不會退出,例如:

func leak2() {
ch := make(chan int)

// 消費者 g1
go func() {
for result := range ch {
fmt.Printf("result: %d\n", result)
}
}()

// 生產(chǎn)者 g2
ch <- 1
ch <- 2
time.Sleep(time.Second) // 模擬耗時
fmt.Println("main goroutine g2 done...")
}

這種情況下,只需要增加 close(ch) 的操作即可,for-range 操作在收到 close 的信號后會退出、goroutine 不再阻塞,能夠被回收。

3.4 如何預(yù)防內(nèi)存泄漏?

預(yù)防 goroutine 泄漏的核心就是:

  • 創(chuàng)建 goroutine 時就要想清楚它什么時候被回收。

具體到執(zhí)行層面,包括:

  • 當(dāng) goroutine 退出時,需要考慮它使用的 channel 有沒有可能阻塞對應(yīng)的生產(chǎn)者、消費者的 goroutine。
  • 盡量使用buffered channel?使用buffered channel 能減少阻塞發(fā)生、即使疏忽了一些極端情況,也能降低 goroutine 泄漏的概率。

4. panic

panic 就更刺激了,一般是測試的時候沒發(fā)現(xiàn),上線之后偶現(xiàn),程序掛掉,服務(wù)出現(xiàn)一個超時毛刺后觸發(fā)告警。channel 導(dǎo)致的 panic 一般是以下幾個原因:

4.1 向已經(jīng) close 掉的 channel 繼續(xù)發(fā)送數(shù)據(jù)

先舉一個簡單的栗子:

func p1() {
ch := make(chan int, 1)
close(ch)
ch <- 1
}
// panic: send on closed channel

在實際開發(fā)過程中,處理多個 goroutine 之間協(xié)作時,可能存在一個 goroutine 已經(jīng) close 掉 channel 了,另外一個不知道,也去 close 一下,就會 panic 掉,例如:

func p1() {
ch := make(chan int, 1)
done := make(chan struct{}, 1)
go func() {
<- time.After(2*time.Second)
println("close2")
close(ch)
close(done)
}()
go func() {
<- time.After(1*time.Second)
println("close1")
ch <- 1
close(ch)
}()

<-done
}

萬惡之源就是在 go 語言里,你是無法知道一個 channel 是否已經(jīng)被 close 掉的,所以在嘗試做 close 操作的時候,就應(yīng)該做好會 panic 的準(zhǔn)備……

4.2 多次 close 同一個 channel

同上,在嘗試往 channel 里發(fā)送數(shù)據(jù)時,就應(yīng)該考慮。

這個 channel 已經(jīng)關(guān)了嗎?

這個 channel 什么時候、在哪個 goroutine 里關(guān)呢?

誰來關(guān)呢?還是干脆不關(guān)?

5. 如何優(yōu)雅地 close channel

5.1 我們需要檢查 channel 是否關(guān)閉嗎?

剛遇到上面說的 panic 問題時,我也試過去找一個內(nèi)置的 closed 函數(shù)來檢查關(guān)閉狀態(tài),結(jié)果發(fā)現(xiàn),并沒有這樣一個函數(shù)……

那么,如果有這樣的函數(shù),真能徹底解決 panic 的問題么?答案是不能。因為 channel 是在一個并發(fā)的環(huán)境下去做收發(fā)操作,就算當(dāng)前執(zhí)行 closed(ch) 得到的結(jié)果是 false,還是不能直接去關(guān),例如如下 yy 出來的代碼:

if !closed(ch) {  // 返回 false
// 在這中間出了幺蛾子!
close(ch) // 還是 panic 了……
}

遵循 less is more 的原則,這個 closed 函數(shù)是要不得了

5.2 需要 close 嗎?為什么?

結(jié)論:除非必須關(guān)閉 chan,否則不要主動關(guān)閉。關(guān)閉 chan 最優(yōu)雅的方式,就是不要關(guān)閉 chan~。

當(dāng)一個 chan 沒有 sender 和 receiver 時,即不再被使用時,GC 會在一段時間后標(biāo)記、清理掉這個 chan。那么什么時候必須關(guān)閉 chan 呢?比較常見的是將 close 作為一種通知機制,尤其是生產(chǎn)者與消費者之間是 1:M 的關(guān)系時,通過 close 告訴下游:我收工了,你們別讀了。

5.3 誰來關(guān)?

chan 關(guān)閉的原則:

  • Don't close a channel from the receiver side 不要在消費者端關(guān)閉 chan。
  • Don't close a channel if the channel has multiple concurrent senders  有多個并發(fā)寫的生產(chǎn)者時也別關(guān)。

只要我們遵循這兩條原則,就能避免兩種 panic 的場景,即:向 closed chan 發(fā)送數(shù)據(jù),或者是 close 一個 closed chan。

按照生產(chǎn)者和消費者的關(guān)系可以拆解成以下幾類情況:

  • 一寫一讀:生產(chǎn)者關(guān)閉即可。
  • 一寫多讀:生產(chǎn)者關(guān)閉即可,關(guān)閉時下游全部消費者都能收到通知。
  • 多寫一讀:多個生產(chǎn)者之間需要引入一個協(xié)調(diào) channel 來處理信號。
  • 多寫多讀:與 3 類似,核心思路是引入一個中間層以及使用try-send 的套路來處理非阻塞的寫入,例如:
func main() {
rand.Seed(time.Now().UnixNano())
log.SetFlags(0)

const Max = 100000
const NumReceivers = 10
const NumSenders = 1000

wgReceivers := sync.WaitGroup{}
wgReceivers.Add(NumReceivers)

dataCh := make(chan int)
stopCh := make(chan struct{})
// stopCh 是額外引入的一個信號 channel.
// 它的生產(chǎn)者是下面的 toStop channel,
// 消費者是上面 dataCh 的生產(chǎn)者和消費者
toStop := make(chan string, 1)
// toStop 是拿來關(guān)閉 stopCh 用的,由 dataCh 的生產(chǎn)者和消費者寫入
// 由下面的匿名中介函數(shù)(moderator)消費
// 要注意,這個一定要是 buffered channel (否則沒法用 try-send 來處理了)

var stoppedBy string

// moderator
go func() {
stoppedBy = <-toStop
close(stopCh)
}()

// senders
for i := 0; i < NumSenders; i++ {
go func(id string) {
for {
value := rand.Intn(Max)
if value == 0 {
// try-send 操作
// 如果 toStop 滿了,就會走 default 分支啥也不干,也不會阻塞
select {
case toStop <- "sender#" + id:
default:
}
return
}


// try-receive 操作,盡快退出
// 如果沒有這一步,下面的 select 操作可能造成 panic
select {
case <- stopCh:
return
default:
}

// 如果嘗試從 stopCh 取數(shù)據(jù)的同時,也嘗試向 dataCh
// 寫數(shù)據(jù),則會命中 select 的偽隨機邏輯,可能會寫入數(shù)據(jù)
select {
case <- stopCh:
return
case dataCh <- value:
}
}
}(strconv.Itoa(i))
}

// receivers
for i := 0; i < NumReceivers; i++ {
go func(id string) {
defer wgReceivers.Done()

for {
// 同上
select {
case <- stopCh:
return
default:
}

// 嘗試讀數(shù)據(jù)
select {
case <- stopCh:
return
case value := <-dataCh:
if value == Max-1 {
select {
case toStop <- "receiver#" + id:
default:
}
return
}

log.Println(value)
}
}
}(strconv.Itoa(i))
}

wgReceivers.Wait()
log.Println("stopped by", stoppedBy)
}

參考資料

  • Golang channel 死鎖的幾種情況以及例子
  • 老手也常誤用!詳解 Go channel 內(nèi)存泄漏問題
  • 深入解析 Goroutine 泄露的場景:channel 發(fā)送者
  • How to Gracefully Close Channels

本文轉(zhuǎn)載自微信公眾號「 翔叔架構(gòu)筆記」,可以通過以下二維碼關(guān)注。轉(zhuǎn)載本文請聯(lián)系翔叔架構(gòu)筆記公眾號。


文章名稱:Golang Channel 三大坑,你踩過了嘛?
轉(zhuǎn)載源于:http://m.5511xx.com/article/cccojsc.html