亚洲乱码中文字幕综合,中国熟女仑乱hd,亚洲精品乱拍国产一区二区三区,一本大道卡一卡二卡三乱码全集资源,又粗又黄又硬又爽的免费视频

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

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

一、channel 的概念

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

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

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

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

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

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

二、channel 的使用

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

channel 的創(chuàng)建

    ch := make(chan int)

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

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

ch := make(chan int, 2)

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

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

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

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

使用 channel 時我們還可以控制 channel 只讀只寫操作:

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

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

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

channel 的讀寫

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

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

對應的操作:

    data <- ch

當我們不再使用 channel 的時候,可以對其進行關(guān)閉:

    close(ch)

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

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

如果關(guān)閉后的 channel 沒有數(shù)據(jù)可讀取時,將得到零值,即對應類型的默認值。

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

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

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

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

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

channel 和 select

在寫程序時,有時并不單單只會和一個 goroutine 通信,當我們要進行多 goroutine 通信時,則會使用 select 寫法來管理多個 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

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

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

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

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

三、channel 的底層原理

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

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

channel 在進行讀寫數(shù)據(jù)時,會根據(jù)無緩沖、有緩沖設(shè)置進行對應的阻塞喚起動作,它們之間還是有區(qū)別的。下面我們來捋一下這些不同之處。

無緩沖 channel

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

channel 先寫再讀

在這里,我們暫時認為有 2 個 goroutine 在使用 channel 通信,按先寫再讀的順序,則具體流程如下:

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

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

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

channel 先讀再寫

先讀再寫的流程跟上面一樣。

G1 暫時被掛在了 recvq 隊列,然后休眠起來。

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

有緩沖 channel

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

channel 先寫再讀

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

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

channel 先讀再寫

此種情況和無緩沖的先讀再寫是一樣流程,此處不再重復說明。

總結(jié)

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

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

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

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

相關(guān)文章

  • go調(diào)用shell命令兩種方式實現(xiàn)(有無返回值)

    go調(diào)用shell命令兩種方式實現(xiàn)(有無返回值)

    本文主要介紹了go調(diào)用shell命令兩種方式實現(xiàn)(有無返回值),主要用于執(zhí)行shell命令,并且返回shell的標準輸出,具有一定的參考價值,感興趣的可以了解一下
    2021-12-12
  • Golang中的同步工具sync.Map示例詳解

    Golang中的同步工具sync.Map示例詳解

    sync.Map是Golang標準庫提供的并發(fā)安全的Map類型,可以在多個goroutine并發(fā)讀寫Map的場景下不需要加鎖,這篇文章主要介紹了Golang中的同步工具sync.Map詳解,需要的朋友可以參考下
    2023-05-05
  • Go語言中的原子操作使用詳解

    Go語言中的原子操作使用詳解

    這篇文章主要介紹了Go語言中的原子操作使用詳解的相關(guān)資料,需要的朋友可以參考下
    2023-08-08
  • Golang token的生成和解析詳解

    Golang token的生成和解析詳解

    這篇文章主要給大家介紹了Golang token的生成和解析,文中通過代碼示例給大家介紹的非常詳細,對大家的學習或工作有一定的幫助,需要的朋友可以參考下
    2024-02-02
  • Go語言題解LeetCode561數(shù)組拆分

    Go語言題解LeetCode561數(shù)組拆分

    這篇文章主要為大家介紹了Go語言題解LeetCode561數(shù)組拆分示例詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進步,早日升職加薪
    2022-12-12
  • 簡單談談Golang中的字符串與字節(jié)數(shù)組

    簡單談談Golang中的字符串與字節(jié)數(shù)組

    這篇文章主要給大家介紹了關(guān)于Golang中字符串與字節(jié)數(shù)組的相關(guān)資料,文中通過示例代碼介紹的非常詳細,對大家的學習或者使用Golang具有一定的參考學習價值,需要的朋友們下面來一起學習學習吧
    2019-03-03
  • Go語言并發(fā)處理效率響應能力及在現(xiàn)代軟件開發(fā)中的重要性

    Go語言并發(fā)處理效率響應能力及在現(xiàn)代軟件開發(fā)中的重要性

    這篇文章主要為大家介紹了Go語言并發(fā)處理的效率及響應能力以及在現(xiàn)代軟件開發(fā)中的重要性實例詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進步,早日升職加薪
    2023-12-12
  • golang中sync.Map并發(fā)創(chuàng)建、讀取問題實戰(zhàn)記錄

    golang中sync.Map并發(fā)創(chuàng)建、讀取問題實戰(zhàn)記錄

    這篇文章主要給大家介紹了關(guān)于golang中sync.Map并發(fā)創(chuàng)建、讀取問題的相關(guān)資料,文中通過示例代碼介紹的非常詳細,對大家的學習或者工作具有一定的參考學習價值,需要的朋友們下面隨著小編來一起學習學習吧
    2018-07-07
  • GO制作微信機器人的流程分析

    GO制作微信機器人的流程分析

    這篇文章主要介紹了利用go制作微信機器人,本文主要包括項目基礎(chǔ)配置及詳細代碼講解,本文給大家介紹的非常詳細,對大家的學習或工作具有一定的參考借鑒價值,需要的朋友可以參考下
    2022-08-08
  • Go手寫數(shù)據(jù)庫ZiyiDB的實現(xiàn)

    Go手寫數(shù)據(jù)庫ZiyiDB的實現(xiàn)

    本文主要介紹了Go手寫數(shù)據(jù)庫ZiyiDB的實現(xiàn),文中通過示例代碼介紹的非常詳細,對大家的學習或者工作具有一定的參考學習價值,需要的朋友們下面隨著小編來一起學習學習吧
    2025-05-05

最新評論