1. <strong id="7actg"></strong>
    2. <table id="7actg"></table>

    3. <address id="7actg"></address>
      <address id="7actg"></address>
      1. <object id="7actg"><tt id="7actg"></tt></object>

        Go 經(jīng)典入門(mén)系列 22:Channel

        共 885字,需瀏覽 2分鐘

         ·

        2020-12-22 13:56

        點(diǎn)擊上方藍(lán)色“Go語(yǔ)言中文網(wǎng)”關(guān)注,每天一起學(xué) Go

        歡迎來(lái)到 Golang 系列教程[1]的第 22 篇。

        上一教程里,我們探討了如何使用 Go 協(xié)程(Goroutine)來(lái)實(shí)現(xiàn)并發(fā)。我們接著在本教程里學(xué)習(xí)信道(Channel),學(xué)習(xí)如何通過(guò)信道來(lái)實(shí)現(xiàn) Go 協(xié)程間的通信。

        什么是信道?

        信道可以想像成 Go 協(xié)程之間通信的管道。如同管道中的水會(huì)從一端流到另一端,通過(guò)使用信道,數(shù)據(jù)也可以從一端發(fā)送,在另一端接收。

        信道的聲明

        所有信道都關(guān)聯(lián)了一個(gè)類(lèi)型。信道只能運(yùn)輸這種類(lèi)型的數(shù)據(jù),而運(yùn)輸其他類(lèi)型的數(shù)據(jù)都是非法的。

        chan T 表示 T 類(lèi)型的信道。

        信道的零值為 nil。信道的零值沒(méi)有什么用,應(yīng)該像對(duì) map 和切片所做的那樣,用 make 來(lái)定義信道。

        下面編寫(xiě)代碼,聲明一個(gè)信道。

        package?main

        import?"fmt"

        func?main()?{
        ?var?a?chan?int
        ?if?a?==?nil?{
        ??fmt.Println("channel?a?is?nil,?going?to?define?it")
        ??a?=?make(chan?int)
        ??fmt.Printf("Type?of?a?is?%T",?a)
        ?}
        }

        在線(xiàn)運(yùn)行程序[2]

        由于信道的零值為 nil,在第 6 行,信道 a 的值就是 nil。于是,程序執(zhí)行了 if 語(yǔ)句內(nèi)的語(yǔ)句,定義了信道 a。程序中 a 是一個(gè) int 類(lèi)型的信道。該程序會(huì)輸出:

        channel?a?is?nil,?going?to?define?it
        Type?of?a?is?chan?int

        簡(jiǎn)短聲明通常也是一種定義信道的簡(jiǎn)潔有效的方法。

        a?:=?make(chan?int)

        這一行代碼同樣定義了一個(gè) int 類(lèi)型的信道 a。

        通過(guò)信道進(jìn)行發(fā)送和接收

        如下所示,該語(yǔ)法通過(guò)信道發(fā)送和接收數(shù)據(jù)。

        data?:=?<-?a?//?讀取信道?a
        a?<-?data?//?寫(xiě)入信道?a

        信道旁的箭頭方向指定了是發(fā)送數(shù)據(jù)還是接收數(shù)據(jù)。

        在第一行,箭頭對(duì)于 a 來(lái)說(shuō)是向外指的,因此我們讀取了信道 a 的值,并把該值存儲(chǔ)到變量 data。

        在第二行,箭頭指向了 a,因此我們?cè)诎褦?shù)據(jù)寫(xiě)入信道 a。

        發(fā)送與接收默認(rèn)是阻塞的

        發(fā)送與接收默認(rèn)是阻塞的。這是什么意思?當(dāng)把數(shù)據(jù)發(fā)送到信道時(shí),程序控制會(huì)在發(fā)送數(shù)據(jù)的語(yǔ)句處發(fā)生阻塞,直到有其它 Go 協(xié)程從信道讀取到數(shù)據(jù),才會(huì)解除阻塞。與此類(lèi)似,當(dāng)讀取信道的數(shù)據(jù)時(shí),如果沒(méi)有其它的協(xié)程把數(shù)據(jù)寫(xiě)入到這個(gè)信道,那么讀取過(guò)程就會(huì)一直阻塞著。

        信道的這種特性能夠幫助 Go 協(xié)程之間進(jìn)行高效的通信,不需要用到其他編程語(yǔ)言常見(jiàn)的顯式鎖或條件變量。

        信道的代碼示例

        理論已經(jīng)夠了:)。接下來(lái)寫(xiě)點(diǎn)代碼,看看協(xié)程之間通過(guò)信道是怎么通信的吧。

        我們其實(shí)可以重寫(xiě)上章學(xué)習(xí) Go 協(xié)程[3] 時(shí)寫(xiě)的程序,現(xiàn)在我們?cè)谶@里用上信道。

        首先引用前面教程里的程序。

        package?main

        import?(
        ?"fmt"
        ?"time"
        )

        func?hello()?{
        ?fmt.Println("Hello?world?goroutine")
        }
        func?main()?{
        ?go?hello()
        ?time.Sleep(1?*?time.Second)
        ?fmt.Println("main?function")
        }

        在線(xiàn)運(yùn)行程序[4]

        這是上一篇的代碼。我們使用到了休眠,使 Go 主協(xié)程等待 hello 協(xié)程結(jié)束。如果你看不懂,建議你閱讀上一教程 Go 協(xié)程[5]。

        我們接下來(lái)使用信道來(lái)重寫(xiě)上面代碼。

        package?main

        import?(
        ?"fmt"
        )

        func?hello(done?chan?bool)?{
        ?fmt.Println("Hello?world?goroutine")
        ?done?<-?true
        }
        func?main()?{
        ?done?:=?make(chan?bool)
        ?go?hello(done)
        ?<-done
        ?fmt.Println("main?function")
        }

        在線(xiàn)運(yùn)行程序[6]

        在上述程序里,我們?cè)诘?12 行創(chuàng)建了一個(gè) bool 類(lèi)型的信道 done,并把 done 作為參數(shù)傳遞給了 hello 協(xié)程。在第 14 行,我們通過(guò)信道 done 接收數(shù)據(jù)。這一行代碼發(fā)生了阻塞,除非有協(xié)程向 done 寫(xiě)入數(shù)據(jù),否則程序不會(huì)跳到下一行代碼。于是,這就不需要用以前的 time.Sleep 來(lái)阻止 Go 主協(xié)程退出了。

        <-done 這行代碼通過(guò)協(xié)程(譯注:原文筆誤,信道)done 接收數(shù)據(jù),但并沒(méi)有使用數(shù)據(jù)或者把數(shù)據(jù)存儲(chǔ)到變量中。這完全是合法的。

        現(xiàn)在我們的 Go 主協(xié)程發(fā)生了阻塞,等待信道 done 發(fā)送的數(shù)據(jù)。該信道作為參數(shù)傳遞給了協(xié)程 hellohello 打印出 Hello world goroutine,接下來(lái)向 done 寫(xiě)入數(shù)據(jù)。當(dāng)完成寫(xiě)入時(shí),Go 主協(xié)程會(huì)通過(guò)信道 done 接收數(shù)據(jù),于是它解除阻塞狀態(tài),打印出文本 main function。

        該程序輸出如下:

        Hello?world?goroutine
        main?function

        我們稍微修改一下程序,在 hello 協(xié)程里加入休眠函數(shù),以便更好地理解阻塞的概念。

        package?main

        import?(
        ?"fmt"
        ?"time"
        )

        func?hello(done?chan?bool)?{
        ?fmt.Println("hello?go?routine?is?going?to?sleep")
        ?time.Sleep(4?*?time.Second)
        ?fmt.Println("hello?go?routine?awake?and?going?to?write?to?done")
        ?done?<-?true
        }
        func?main()?{
        ?done?:=?make(chan?bool)
        ?fmt.Println("Main?going?to?call?hello?go?goroutine")
        ?go?hello(done)
        ?<-done
        ?fmt.Println("Main?received?data")
        }

        在線(xiàn)運(yùn)行程序[7]

        在上面程序里,我們向 hello 函數(shù)里添加了 4 秒的休眠(第 10 行)。

        程序首先會(huì)打印 Main going to call hello go goroutine。接著會(huì)開(kāi)啟 hello 協(xié)程,打印 hello go routine is going to sleep。打印完之后,hello 協(xié)程會(huì)休眠 4 秒鐘,而在這期間,主協(xié)程會(huì)在 <-done 這一行發(fā)生阻塞,等待來(lái)自信道 done 的數(shù)據(jù)。4 秒鐘之后,打印 hello go routine awake and going to write to done,接著再打印 Main received data。

        信道的另一個(gè)示例

        我們?cè)倬帉?xiě)一個(gè)程序來(lái)更好地理解信道。該程序會(huì)計(jì)算一個(gè)數(shù)中每一位的平方和與立方和,然后把平方和與立方和相加并打印出來(lái)。

        例如,如果輸出是 123,該程序會(huì)如下計(jì)算輸出:

        squares?=?(1?*?1)?+?(2?*?2)?+?(3?*?3)
        cubes?=?(1?*?1?*?1)?+?(2?*?2?*?2)?+?(3?*?3?*?3)
        output?=?squares?+?cubes?=?50

        我們會(huì)這樣去構(gòu)建程序:在一個(gè)單獨(dú)的 Go 協(xié)程計(jì)算平方和,而在另一個(gè)協(xié)程計(jì)算立方和,最后在 Go 主協(xié)程把平方和與立方和相加。

        package?main

        import?(
        ?"fmt"
        )

        func?calcSquares(number?int,?squareop?chan?int)?{
        ?sum?:=?0
        ?for?number?!=?0?{
        ??digit?:=?number?%?10
        ??sum?+=?digit?*?digit
        ??number?/=?10
        ?}
        ?squareop?<-?sum
        }

        func?calcCubes(number?int,?cubeop?chan?int)?{
        ?sum?:=?0
        ?for?number?!=?0?{
        ??digit?:=?number?%?10
        ??sum?+=?digit?*?digit?*?digit
        ??number?/=?10
        ?}
        ?cubeop?<-?sum
        }

        func?main()?{
        ?number?:=?589
        ?sqrch?:=?make(chan?int)
        ?cubech?:=?make(chan?int)
        ?go?calcSquares(number,?sqrch)
        ?go?calcCubes(number,?cubech)
        ?squares,?cubes?:=?<-sqrch,?<-cubech
        ?fmt.Println("Final?output",?squares?+?cubes)
        }

        在線(xiàn)運(yùn)行程序[8]

        在第 7 行,函數(shù) calcSquares 計(jì)算一個(gè)數(shù)每位的平方和,并把結(jié)果發(fā)送給信道 squareop。與此類(lèi)似,在第 17 行函數(shù) calcCubes 計(jì)算一個(gè)數(shù)每位的立方和,并把結(jié)果發(fā)送給信道 cubop。

        這兩個(gè)函數(shù)分別在單獨(dú)的協(xié)程里運(yùn)行(第 31 行和第 32 行),每個(gè)函數(shù)都有傳遞信道的參數(shù),以便寫(xiě)入數(shù)據(jù)。Go 主協(xié)程會(huì)在第 33 行等待兩個(gè)信道傳來(lái)的數(shù)據(jù)。一旦從兩個(gè)信道接收完數(shù)據(jù),數(shù)據(jù)就會(huì)存儲(chǔ)在變量 squarescubes 里,然后計(jì)算并打印出最后結(jié)果。該程序會(huì)輸出:

        Final?output?1536

        死鎖

        使用信道需要考慮的一個(gè)重點(diǎn)是死鎖。當(dāng) Go 協(xié)程給一個(gè)信道發(fā)送數(shù)據(jù)時(shí),照理說(shuō)會(huì)有其他 Go 協(xié)程來(lái)接收數(shù)據(jù)。如果沒(méi)有的話(huà),程序就會(huì)在運(yùn)行時(shí)觸發(fā) panic,形成死鎖。

        同理,當(dāng)有 Go 協(xié)程等著從一個(gè)信道接收數(shù)據(jù)時(shí),我們期望其他的 Go 協(xié)程會(huì)向該信道寫(xiě)入數(shù)據(jù),要不然程序就會(huì)觸發(fā) panic。

        package?main

        func?main()?{
        ?ch?:=?make(chan?int)
        ?ch?<-?5
        }

        在線(xiàn)運(yùn)行程序[9]

        在上述程序中,我們創(chuàng)建了一個(gè)信道 ch,接著在下一行 ch <- 5,我們把 5 發(fā)送到這個(gè)信道。對(duì)于本程序,沒(méi)有其他的協(xié)程從 ch 接收數(shù)據(jù)。于是程序觸發(fā) panic,出現(xiàn)如下運(yùn)行時(shí)錯(cuò)誤。

        fatal?error:?all?goroutines?are?asleep?-?deadlock!

        goroutine?1?[chan?send]:
        main.main()
        ?/tmp/sandbox249677995/main.go:6?+0x80

        單向信道

        我們目前討論的信道都是雙向信道,即通過(guò)信道既能發(fā)送數(shù)據(jù),又能接收數(shù)據(jù)。其實(shí)也可以創(chuàng)建單向信道,這種信道只能發(fā)送或者接收數(shù)據(jù)。

        package?main

        import?"fmt"

        func?sendData(sendch?chan<-?int)?{
        ?sendch?<-?10
        }

        func?main()?{
        ?sendch?:=?make(chan<-?int)
        ?go?sendData(sendch)
        ?fmt.Println(<-sendch)
        }

        在線(xiàn)運(yùn)行程序[10]

        上面程序的第 10 行,我們創(chuàng)建了唯送(Send Only)信道 sendchchan<- int 定義了唯送信道,因?yàn)榧^指向了 chan。在第 12 行,我們?cè)噲D通過(guò)唯送信道接收數(shù)據(jù),于是編譯器報(bào)錯(cuò):

        main.go:11:?invalid?operation:?<-sendch?(receive?from?send-only?type?chan<-?int)

        一切都很順利,只不過(guò)一個(gè)不能讀取數(shù)據(jù)的唯送信道究竟有什么意義呢?

        這就需要用到信道轉(zhuǎn)換(Channel Conversion)了。把一個(gè)雙向信道轉(zhuǎn)換成唯送信道或者唯收(Receive Only)信道都是行得通的,但是反過(guò)來(lái)就不行。

        package?main

        import?"fmt"

        func?sendData(sendch?chan<-?int)?{
        ?sendch?<-?10
        }

        func?main()?{
        ?cha1?:=?make(chan?int)
        ?go?sendData(cha1)
        ?fmt.Println(<-cha1)
        }

        在線(xiàn)運(yùn)行程序[11]

        在上述程序的第 10 行,我們創(chuàng)建了一個(gè)雙向信道 cha1。在第 11 行 cha1 作為參數(shù)傳遞給了 sendData 協(xié)程。在第 5 行,函數(shù) sendData 里的參數(shù) sendch chan<- intcha1 轉(zhuǎn)換為一個(gè)唯送信道。于是該信道在 sendData 協(xié)程里是一個(gè)唯送信道,而在 Go 主協(xié)程里是一個(gè)雙向信道。該程序最終打印輸出 10。

        關(guān)閉信道和使用 for range 遍歷信道

        數(shù)據(jù)發(fā)送方可以關(guān)閉信道,通知接收方這個(gè)信道不再有數(shù)據(jù)發(fā)送過(guò)來(lái)。

        當(dāng)從信道接收數(shù)據(jù)時(shí),接收方可以多用一個(gè)變量來(lái)檢查信道是否已經(jīng)關(guān)閉。

        v,?ok?:=?<-?ch

        上面的語(yǔ)句里,如果成功接收信道所發(fā)送的數(shù)據(jù),那么 ok 等于 true。而如果 ok 等于 false,說(shuō)明我們?cè)噲D讀取一個(gè)關(guān)閉的通道。從關(guān)閉的信道讀取到的值會(huì)是該信道類(lèi)型的零值。例如,當(dāng)信道是一個(gè) int 類(lèi)型的信道時(shí),那么從關(guān)閉的信道讀取的值將會(huì)是 0。

        package?main

        import?(
        ?"fmt"
        )

        func?producer(chnl?chan?int)?{
        ?for?i?:=?0;?i?10;?i++?{
        ??chnl?<-?i
        ?}
        ?close(chnl)
        }
        func?main()?{
        ?ch?:=?make(chan?int)
        ?go?producer(ch)
        ?for?{
        ??v,?ok?:=?<-ch
        ??if?ok?==?false?{
        ???break
        ??}
        ??fmt.Println("Received?",?v,?ok)
        ?}
        }

        在線(xiàn)運(yùn)行程序[12]

        在上述的程序中,producer 協(xié)程會(huì)從 0 到 9 寫(xiě)入信道 chn1,然后關(guān)閉該信道。主函數(shù)有一個(gè)無(wú)限的 for 循環(huán)(第 16 行),使用變量 ok(第 18 行)檢查信道是否已經(jīng)關(guān)閉。如果 ok 等于 false,說(shuō)明信道已經(jīng)關(guān)閉,于是退出 for 循環(huán)。如果 ok 等于 true,會(huì)打印出接收到的值和 ok 的值。

        Received??0?true
        Received??1?true
        Received??2?true
        Received??3?true
        Received??4?true
        Received??5?true
        Received??6?true
        Received??7?true
        Received??8?true
        Received??9?true

        for range 循環(huán)用于在一個(gè)信道關(guān)閉之前,從信道接收數(shù)據(jù)。

        接下來(lái)我們使用 for range 循環(huán)重寫(xiě)上面的代碼。

        package?main

        import?(
        ?"fmt"
        )

        func?producer(chnl?chan?int)?{
        ?for?i?:=?0;?i?10;?i++?{
        ??chnl?<-?i
        ?}
        ?close(chnl)
        }
        func?main()?{
        ?ch?:=?make(chan?int)
        ?go?producer(ch)
        ?for?v?:=?range?ch?{
        ??fmt.Println("Received?",v)
        ?}
        }

        在線(xiàn)運(yùn)行程序[13]

        在第 16 行,for range 循環(huán)從信道 ch 接收數(shù)據(jù),直到該信道關(guān)閉。一旦關(guān)閉了 ch,循環(huán)會(huì)自動(dòng)結(jié)束。該程序會(huì)輸出:

        Received??0
        Received??1
        Received??2
        Received??3
        Received??4
        Received??5
        Received??6
        Received??7
        Received??8
        Received??9

        我們可以使用 for range 循環(huán),重寫(xiě)信道的另一個(gè)示例[14]這一節(jié)里面的代碼,提高代碼的可重用性。

        如果你仔細(xì)觀察這段代碼,會(huì)發(fā)現(xiàn)獲得一個(gè)數(shù)里的每位數(shù)的代碼在 calcSquarescalcCubes 兩個(gè)函數(shù)內(nèi)重復(fù)了。我們將把這段代碼抽離出來(lái),放在一個(gè)單獨(dú)的函數(shù)里,然后并發(fā)地調(diào)用它。

        package?main

        import?(
        ?"fmt"
        )

        func?digits(number?int,?dchnl?chan?int)?{
        ?for?number?!=?0?{
        ??digit?:=?number?%?10
        ??dchnl?<-?digit
        ??number?/=?10
        ?}
        ?close(dchnl)
        }
        func?calcSquares(number?int,?squareop?chan?int)?{
        ?sum?:=?0
        ?dch?:=?make(chan?int)
        ?go?digits(number,?dch)
        ?for?digit?:=?range?dch?{
        ??sum?+=?digit?*?digit
        ?}
        ?squareop?<-?sum
        }

        func?calcCubes(number?int,?cubeop?chan?int)?{
        ?sum?:=?0
        ?dch?:=?make(chan?int)
        ?go?digits(number,?dch)
        ?for?digit?:=?range?dch?{
        ??sum?+=?digit?*?digit?*?digit
        ?}
        ?cubeop?<-?sum
        }

        func?main()?{
        ?number?:=?589
        ?sqrch?:=?make(chan?int)
        ?cubech?:=?make(chan?int)
        ?go?calcSquares(number,?sqrch)
        ?go?calcCubes(number,?cubech)
        ?squares,?cubes?:=?<-sqrch,?<-cubech
        ?fmt.Println("Final?output",?squares+cubes)
        }

        在線(xiàn)運(yùn)行程序[15]

        上述程序里的 digits 函數(shù),包含了獲取一個(gè)數(shù)的每位數(shù)的邏輯,并且 calcSquarescalcCubes 兩個(gè)函數(shù)并發(fā)地調(diào)用了 digits。當(dāng)計(jì)算完數(shù)字里面的每一位數(shù)時(shí),第 13 行就會(huì)關(guān)閉信道。calcSquarescalcCubes 兩個(gè)協(xié)程使用 for range 循環(huán)分別監(jiān)聽(tīng)了它們的信道,直到該信道關(guān)閉。程序的其他地方不變,該程序同樣會(huì)輸出:

        Final?output?1536

        本教程的內(nèi)容到此結(jié)束。關(guān)于信道還有一些其他的概念,比如緩沖信道(Buffered Channel)、工作池(Worker Pool)和 select。我們會(huì)在接下來(lái)的教程里專(zhuān)門(mén)介紹它們。感謝閱讀。祝你愉快。

        上一教程 - Go 協(xié)程

        下一教程 - 緩沖信道和工作池[16]


        via: https://golangbot.com/channels/

        作者:Nick Coghlan[17]譯者:Noluye[18]校對(duì):polaris1119[19]

        本文由 GCTT[20] 原創(chuàng)編譯,Go 中文網(wǎng)[21] 榮譽(yù)推出

        參考資料

        [1]

        Golang 系列教程: https://studygolang.com/subject/2

        [2]

        在線(xiàn)運(yùn)行程序: https://play.golang.org/p/QDtf6mvymD

        [3]

        Go 協(xié)程: https://studygolang.com/articles/12342

        [4]

        在線(xiàn)運(yùn)行程序: https://play.golang.org/p/U9ZZuSql8-

        [5]

        Go 協(xié)程: https://studygolang.com/articles/12342

        [6]

        在線(xiàn)運(yùn)行程序: https://play.golang.org/p/I8goKv6ZMF

        [7]

        在線(xiàn)運(yùn)行程序: https://play.golang.org/p/EejiO-yjUQ

        [8]

        在線(xiàn)運(yùn)行程序: https://play.golang.org/p/4RKr7_YO_B

        [9]

        在線(xiàn)運(yùn)行程序: https://play.golang.org/p/q1O5sNx4aW

        [10]

        在線(xiàn)運(yùn)行程序: https://play.golang.org/p/PRKHxM-iRK

        [11]

        在線(xiàn)運(yùn)行程序: https://play.golang.org/p/aqi_rJ1U8j

        [12]

        在線(xiàn)運(yùn)行程序: https://play.golang.org/p/XWmUKDA2Ri

        [13]

        在線(xiàn)運(yùn)行程序: https://play.golang.org/p/JJ3Ida1r_6

        [14]

        信道的另一個(gè)示例: #

        [15]

        在線(xiàn)運(yùn)行程序: https://play.golang.org/p/oL86W9Ui03

        [16]

        緩沖信道和工作池: https://studygolang.com/articles/12512

        [17]

        Nick Coghlan: https://golangbot.com/about/

        [18]

        Noluye: https://github.com/Noluye

        [19]

        polaris1119: https://github.com/polaris1119

        [20]

        GCTT: https://github.com/studygolang/GCTT

        [21]

        Go 中文網(wǎng): https://studygolang.com/



        推薦閱讀


        福利

        我為大家整理了一份從入門(mén)到進(jìn)階的Go學(xué)習(xí)資料禮包,包含學(xué)習(xí)建議:入門(mén)看什么,進(jìn)階看什么。關(guān)注公眾號(hào) 「polarisxu」,回復(fù) ebook 獲取;還可以回復(fù)「進(jìn)群」,和數(shù)萬(wàn) Gopher 交流學(xué)習(xí)。

        瀏覽 48
        點(diǎn)贊
        評(píng)論
        收藏
        分享

        手機(jī)掃一掃分享

        分享
        舉報(bào)
        評(píng)論
        圖片
        表情
        推薦
        點(diǎn)贊
        評(píng)論
        收藏
        分享

        手機(jī)掃一掃分享

        分享
        舉報(bào)
        1. <strong id="7actg"></strong>
        2. <table id="7actg"></table>

        3. <address id="7actg"></address>
          <address id="7actg"></address>
          1. <object id="7actg"><tt id="7actg"></tt></object>
            国产精品911 | 一级A片一级毛片兔费直播 | 青青草黄色 | 我两腿被同学摸的直流水 | 日韩色一区三区在线 | 久久诱惑 | 日韩在线一区二区免费 | 99re热久久最新地址 | 欧美午夜免费 | 国产亚洲AV片天天在线观看 |