欧美bbbwbbbw肥妇,免费乱码人妻系列日韩,一级黄片

golang?channel多協(xié)程通信常用方法底層原理全面解析

 更新時(shí)間:2023年09月27日 10:01:24   作者:lincoln_hlf1  
channel?是?goroutine?與?goroutine?之間通信的重要橋梁,借助?channel,我們能很輕易的寫(xiě)出一個(gè)多協(xié)程通信程序,今天,我們就來(lái)看看這個(gè)?channel?的常用用法以及底層原理

一、channel 的概念

channel 是一個(gè)通道,用于端到端的數(shù)據(jù)傳輸,這有點(diǎn)像我們平常使用的消息隊(duì)列,只不過(guò) channel 的發(fā)送方和接受方是 goroutine 對(duì)象,屬于內(nèi)存級(jí)別的通信。

這里涉及到了 goroutine 概念,goroutine 是輕量級(jí)的協(xié)程,有屬于自己的??臻g。 我們可以把它理解為線程,只不過(guò) goroutine 的性能開(kāi)銷(xiāo)很小,并且在用戶(hù)態(tài)上實(shí)現(xiàn)了屬于自己的調(diào)度模型。

傳統(tǒng)的線程通信有很多方式,像內(nèi)存共享、信號(hào)量等。其中內(nèi)存共享實(shí)現(xiàn)較為簡(jiǎn)單,只需要對(duì)變量進(jìn)行并發(fā)控制,加鎖即可。但這種在后續(xù)業(yè)務(wù)逐漸復(fù)雜時(shí),將很難維護(hù),耦合性也比較強(qiáng)。

后來(lái)提出了 CSP 模型,即在通信雙方抽象出中間層,數(shù)據(jù)的流轉(zhuǎn)由中間層來(lái)控制,通信雙方只負(fù)責(zé)數(shù)據(jù)的發(fā)送和接收,從而實(shí)現(xiàn)了數(shù)據(jù)的共享,這就是所謂的通過(guò)通信來(lái)共享內(nèi)存。 channel 就是按這個(gè)模型來(lái)實(shí)現(xiàn)的。

channel 在多并發(fā)操作里是屬于協(xié)程安全的,并且遵循了 FIFO 特性。即先執(zhí)行讀取的 goroutine 會(huì)先獲取到數(shù)據(jù),先發(fā)送數(shù)據(jù)的 goroutine 會(huì)先輸入數(shù)據(jù)。

另外,channel 的使用將會(huì)引起 Go runtime 的調(diào)度調(diào)用,會(huì)有阻塞和喚起 goroutine 的情況產(chǎn)生。

二、channel 的使用

在深入了解 channel 的底層之前,我們先來(lái)看看 channel 的常用用法。

channel 的創(chuàng)建

    ch := make(chan int)

上面是創(chuàng)建了無(wú)緩沖的 channel,一旦有 goroutine 往 channel 發(fā)送數(shù)據(jù),那么當(dāng)前的 goroutine 會(huì)被阻塞住,直到有其他的 goroutine 消費(fèi)了 channel 里的數(shù)據(jù),才能繼續(xù)運(yùn)行。

還有另外一種是有緩沖的 channel,它的創(chuàng)建是這樣的:

ch := make(chan int, 2)

第二個(gè)參數(shù)表示 channel 可緩沖數(shù)據(jù)的容量。只要當(dāng)前 channel 里的元素總數(shù)不大于這個(gè)可緩沖容量,則當(dāng)前的 goroutine 就不會(huì)被阻塞住。

需要注意的是,上面 make 后返回的是一個(gè)指向 hchan 結(jié)構(gòu)的指針變量,等會(huì)將會(huì)聊聊 hchan 的底層結(jié)構(gòu)。

另外,我們也可以聲明一個(gè) nil 的 channel,只是創(chuàng)建這樣的 channel 沒(méi)有意義,讀、寫(xiě) channel 都將會(huì)被阻塞住。一般 nil channel 用在 select 上,讓 select 不再?gòu)倪@個(gè) channel 里讀取數(shù)據(jù),如下用法:

    ch1 := make(chan int)
    ch2 := make(chan int)
    go func() {
        if !ok { // 某些原因,設(shè)置 ch1 為 nil
            ch1 = nil
        }
    }()
    for {
        select {
        case <-ch1: // 當(dāng) ch1 被設(shè)置為 nil 后,將不會(huì)到達(dá)此分支了。
            doSomething1()
        case <-ch2:
            doSomething2()
        }
    }

使用 channel 時(shí)我們還可以控制 channel 只讀只寫(xiě)操作:

    func readChan(ch <-chan int){
        // chan 只允許被讀
    }
    func main(){
        ch := make(chan int)
        readChan(ch)
    }

反之,如果要求只寫(xiě)操作,則可以這樣:

    func writeChan(ch chan<- int){
        // chan 只允許被寫(xiě)
    }

channel 的讀寫(xiě)

往一個(gè) channel 發(fā)送數(shù)據(jù),可以這樣寫(xiě)

    ch := make(chan int)
    ch <- 1

對(duì)應(yīng)的操作:

    data <- ch

當(dāng)我們不再使用 channel 的時(shí)候,可以對(duì)其進(jìn)行關(guān)閉:

    close(ch)

當(dāng) channel 被關(guān)閉后,如果繼續(xù)往里面寫(xiě)數(shù)據(jù),則程序會(huì)直接 panic 退出。

不過(guò)讀取關(guān)閉后的 channel,不會(huì)產(chǎn)生 pannic,還是可以讀到數(shù)據(jù)。

如果關(guān)閉后的 channel 沒(méi)有數(shù)據(jù)可讀取時(shí),將得到零值,即對(duì)應(yīng)類(lèi)型的默認(rèn)值。

為了能知道當(dāng)前 channel 是否被關(guān)閉,可以使用下面的寫(xiě)法來(lái)判斷。

    if v, ok := <-ch; !ok {
        fmt.Println("channel 已關(guān)閉,讀取不到數(shù)據(jù)")
    }

還可以使用下面的寫(xiě)法不斷的獲取 channel 里的數(shù)據(jù):

    for data := range ch {
        // get data dosomething
    }

這種用法會(huì)在讀取完 channel 里的數(shù)據(jù)后就結(jié)束 for 循環(huán),執(zhí)行后面的代碼。

channel 和 select

在寫(xiě)程序時(shí),有時(shí)并不單單只會(huì)和一個(gè) goroutine 通信,當(dāng)我們要進(jìn)行多 goroutine 通信時(shí),則會(huì)使用 select 寫(xiě)法來(lái)管理多個(gè) channel 的通信數(shù)據(jù):

    ch1 := make(chan struct{})
    ch2 := make(chan struct{})
    // ch1, ch2 發(fā)送數(shù)據(jù)
    go sendCh1(ch1)
    go sendCh1(ch2)
    // channel 數(shù)據(jù)接受處理
    for {
        select {
        case <-ch1:
            doSomething1()
        case <-ch2:
            doSomething2()
        }
    }

channel 的 deadlock

前面提到過(guò),往 channel 里讀寫(xiě)數(shù)據(jù)時(shí)是有可能被阻塞住的,一旦被阻塞,則需要其他的 goroutine 執(zhí)行對(duì)應(yīng)的讀寫(xiě)操作,才能解除阻塞狀態(tài)。

然而,阻塞后一直沒(méi)能發(fā)生調(diào)度行為,沒(méi)有可用的 goroutine 可執(zhí)行,則會(huì)一直卡在這個(gè)地方,程序就失去執(zhí)行意義了。此時(shí) Go 就會(huì)報(bào) deadlock 錯(cuò)誤,如下代碼:

    func main() {
        ch := make(chan int)
        <-ch
        // 執(zhí)行后將 panic:
        // fatal error: all goroutines are asleep - deadlock!
    }

因此,在使用 channel 時(shí)要注意 goroutine 的一發(fā)一取,避免 goroutine 永久阻塞!

三、channel 的底層原理

前面提及過(guò) channel 創(chuàng)建后返回了 hchan 結(jié)構(gòu)體,現(xiàn)在我們來(lái)研究下這個(gè)結(jié)構(gòu)體,它的主要字段如下:

type hchan struct {
    qcount   uint   // channel 里的元素計(jì)數(shù)
    dataqsiz uint   // 可以緩沖的數(shù)量,如 ch := make(chan int, 10)。 此處的 10 即 dataqsiz
    elemsize uint16 // 要發(fā)送或接收的數(shù)據(jù)類(lèi)型大小
    buf      unsafe.Pointer // 當(dāng) channel 設(shè)置了緩沖數(shù)量時(shí),該 buf 指向一個(gè)存儲(chǔ)緩沖數(shù)據(jù)的區(qū)域,該區(qū)域是一個(gè)循環(huán)隊(duì)列的數(shù)據(jù)結(jié)構(gòu)
    closed   uint32 // 關(guān)閉狀態(tài)
    sendx    uint  // 當(dāng) channel 設(shè)置了緩沖數(shù)量時(shí),數(shù)據(jù)區(qū)域即循環(huán)隊(duì)列此時(shí)已發(fā)送數(shù)據(jù)的索引位置
    recvx    uint  // 當(dāng) channel 設(shè)置了緩沖數(shù)量時(shí),數(shù)據(jù)區(qū)域即循環(huán)隊(duì)列此時(shí)已接收數(shù)據(jù)的索引位置
    recvq    waitq // 想讀取數(shù)據(jù)但又被阻塞住的 goroutine 隊(duì)列
    sendq    waitq // 想發(fā)送數(shù)據(jù)但又被阻塞住的 goroutine 隊(duì)列
    lock mutex
    ...
}

channel 在進(jìn)行讀寫(xiě)數(shù)據(jù)時(shí),會(huì)根據(jù)無(wú)緩沖、有緩沖設(shè)置進(jìn)行對(duì)應(yīng)的阻塞喚起動(dòng)作,它們之間還是有區(qū)別的。下面我們來(lái)捋一下這些不同之處。

無(wú)緩沖 channel

由于對(duì) channel 的讀寫(xiě)先后順序不同,處理也會(huì)有所不同,所以,還得再進(jìn)一步區(qū)分:

channel 先寫(xiě)再讀

在這里,我們暫時(shí)認(rèn)為有 2 個(gè) goroutine 在使用 channel 通信,按先寫(xiě)再讀的順序,則具體流程如下:

可以看到,由于 channel 是無(wú)緩沖的,所以 G1 暫時(shí)被掛在 sendq 隊(duì)列里,然后 G1 調(diào)用了 gopark 休眠了起來(lái)。

接著,又有 goroutine 來(lái) channel 讀取數(shù)據(jù)了:

此時(shí) G2 發(fā)現(xiàn) sendq 等待隊(duì)列里有 goroutine 存在,于是直接從 G1 copy 數(shù)據(jù)過(guò)來(lái),并且會(huì)對(duì) G1 設(shè)置 goready 函數(shù),這樣下次調(diào)度發(fā)生時(shí), G1 就可以繼續(xù)運(yùn)行,并且會(huì)從等待隊(duì)列里移除掉。

channel 先讀再寫(xiě)

先讀再寫(xiě)的流程跟上面一樣。

G1 暫時(shí)被掛在了 recvq 隊(duì)列,然后休眠起來(lái)。

G2 在寫(xiě)數(shù)據(jù)時(shí),發(fā)現(xiàn) recvq 隊(duì)列有 goroutine 存在,于是直接將數(shù)據(jù)發(fā)送給 G1。同時(shí)設(shè)置 G1 goready 函數(shù),等待下次調(diào)度運(yùn)行。

有緩沖 channel

在分析完了無(wú)緩沖 channel 的讀寫(xiě)后,我們繼續(xù)看看有緩沖 channel 的讀寫(xiě)。同樣的,我們分為 2 種情況:

channel 先寫(xiě)再讀

這一次會(huì)優(yōu)先判斷緩沖數(shù)據(jù)區(qū)域是否已滿,如果未滿,則將數(shù)據(jù)保存在緩沖數(shù)據(jù)區(qū)域,即環(huán)形隊(duì)列里。如果已滿,則和之前的流程是一樣的。

當(dāng) G2 要讀取數(shù)據(jù)時(shí),會(huì)優(yōu)先從緩沖數(shù)據(jù)區(qū)域去讀取,并且在讀取完后,會(huì)檢查 sendq 隊(duì)列,如果 goroutine 有等待隊(duì)列,則會(huì)將它上面的 data 補(bǔ)充到緩沖數(shù)據(jù)區(qū)域,并且也對(duì)其設(shè)置 goready 函數(shù)。

channel 先讀再寫(xiě)

此種情況和無(wú)緩沖的先讀再寫(xiě)是一樣流程,此處不再重復(fù)說(shuō)明。

總結(jié)

有緩沖 channel 和無(wú)緩沖 channel 的讀寫(xiě)基本相差不大,只是多了緩沖數(shù)據(jù)區(qū)域的判斷而已。

channel 在使用的時(shí)候大多時(shí)候得和 select 配合使用,盡管只需要簡(jiǎn)單的用 <- ch 和 ch <- 來(lái)讀寫(xiě)數(shù)據(jù),但它的底層還是很有講究的,特別是涉及到調(diào)度的休眠喚起。

這也能看出 Go 的精妙之處:復(fù)雜底層,優(yōu)雅運(yùn)用。

以上就是golang channel多協(xié)程通信常用方法底層原理全面解析的詳細(xì)內(nèi)容,更多關(guān)于golang channel多協(xié)程通信的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!

相關(guān)文章

最新評(píng)論