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

golang踩坑實(shí)戰(zhàn)之channel的正確使用方式

 更新時(shí)間:2023年06月15日 15:06:59   作者:黃楊峻  
Golang?channel是Go語言中一個(gè)非常重要的特性,除了用來處理并發(fā)編程的任務(wù)中,它還可以用來進(jìn)行消息傳遞和事件通知,這篇文章主要給大家介紹了關(guān)于golang踩坑實(shí)戰(zhàn)之channel的正確使用方式,需要的朋友可以參考下

一、為什么要用channel

筆者也是從Java轉(zhuǎn)Go的選手,之前一直很難擺脫線程池、可重入鎖、AQS等數(shù)據(jù)結(jié)構(gòu)及其底層的思維定式。而最近筆者也開始逐漸回顧過往的實(shí)習(xí)和實(shí)驗(yàn),慢慢領(lǐng)悟了golang并發(fā)的一些經(jīng)驗(yàn)了。

golang在解決并發(fā)race問題時(shí),首要考慮的方案是使用channel。可能很多人會(huì)喜歡用互斥鎖sync.Mutex,因?yàn)閙utex lock只有Lock和Unlock兩種操作,與Java中的ReentrantLock比較類似。但筆者實(shí)踐過程中發(fā)現(xiàn):

互斥鎖只能做到阻塞,而無法讓流程之間通信。如果不同流程之間需要交流,則需要一個(gè)類似于信號量一樣的機(jī)制。同時(shí),最好該機(jī)制能實(shí)現(xiàn)流程控制。譬如控制不同任務(wù)執(zhí)行的先后順序,讓任務(wù)等待未完成的任務(wù),以及打斷某個(gè)輪轉(zhuǎn)的狀態(tài)。

如何實(shí)現(xiàn)這些功能?channel就是Go給出的一個(gè)優(yōu)雅的答案。(當(dāng)然并不是說channel可完全替代鎖,鎖可以使得代碼和邏輯更簡單)

二、基本操作

2.1 channel

channel可以看作一個(gè)FIFO的隊(duì)列,隊(duì)列進(jìn)出都是原子操作。隊(duì)列內(nèi)部元素的類型可以自由選擇。以下給出channel的常見操作

//初始化
ss := make(chan struct{})
sb := make(chan bool)
var s chan bool
si = make(chan int)
// 寫
si <- 1
sb <- true
ss <- struct{}
//讀
<-sb
i := <-si
fmt.Print(i+1)//2
// 使用完畢的channel可close
close(si)

2.2 channel緩存

一般來說,channel有帶緩存和不帶緩存兩種。

不帶緩存的channel讀和寫都是阻塞的,一旦某個(gè)channel發(fā)生寫操作,除非另一個(gè)goroutine使用讀操作將元素從channel取出,否則當(dāng)前goroutine會(huì)一直阻塞。反之,如果一個(gè)不帶緩存的channel被一個(gè)goroutine讀取,除非另一個(gè)goroutine對該channel發(fā)起寫入,否則當(dāng)前goroutine會(huì)一直被阻塞。

下面這個(gè)單元測試的結(jié)果是編譯器報(bào)錯(cuò),提示死鎖。

func TestChannel0(t *testing.T) {
	c := make(chan int)
	c <- 1
}

fatal error: all goroutines are asleep - deadlock!

如果要正確運(yùn)行,應(yīng)修改為

func TestChannel0(t *testing.T) {
	c := make(chan int)
	go func(c chan int) { <-c }(c)
	c <- 1
}

帶通道緩存的channel的特點(diǎn)是,有緩存空間時(shí)可以寫入數(shù)據(jù)后直接返回,緩存中有數(shù)據(jù)時(shí)可以直接讀出。如果緩存空間寫滿,同時(shí)沒有被讀取,那寫入會(huì)阻塞。同理,如果緩存空間沒有數(shù)據(jù),讀入也會(huì)阻塞,直到有數(shù)據(jù)被寫入。

//會(huì)成功執(zhí)行
func TestChannel1(t *testing.T) {
	c := make(chan int,1)
	go func(c chan int) { c <- 1 }(c)
	<-c
}

//不會(huì)死鎖,因?yàn)榫彺婵臻g未填滿
func TestChannel2(t *testing.T) {
	c := make(chan int,1)
	c<-1
}

//會(huì)死鎖,因?yàn)榫彺婵臻g填滿后仍繼續(xù)寫入
func TestChannel3(t *testing.T) {
	c := make(chan int,1)
	c<-1
	c<-1
}

//會(huì)死鎖,因?yàn)橐恢弊x取阻塞,沒有寫入
func TestChannel4(t *testing.T) {
	c := make(chan int,1)
	<-c
}

2.3 只讀只寫channel

有些channel可以被定義為只能用于寫入,或者只能用于發(fā)送。

下面是具體例子

func sender(c chan<- bool){
	c <- true
	//<- c // 這一句會(huì)報(bào)錯(cuò)
}
func receiver(c <-chan bool){
	//c <- true// 這一句會(huì)報(bào)錯(cuò)
	<- c
}
func normal(){
	senderChan := make(chan<- bool)
	receiverChan := make(<-chan bool)
}

2.4 select

select允許goroutine對多個(gè)channel操作進(jìn)行同時(shí)監(jiān)聽,當(dāng)某個(gè)case子句可以運(yùn)行時(shí),該case下面的邏輯會(huì)執(zhí)行,且select語句結(jié)束。如果定義了default語句,且各個(gè)case中的執(zhí)行均被阻塞無法完成時(shí),程序便會(huì)進(jìn)入default的邏輯中。

值得注意的是,如果有多個(gè)case可以滿足,最終執(zhí)行的case語句是不確定的(不同于switch語句的從上到下依次判斷是否滿足)。

下面用一個(gè)例子來說明

func writeTrue(c chan bool) {
	c <- false
}
// 輸出為 chan 1, 因?yàn)閏han 1有可讀數(shù)據(jù)
func TestSelect0(t *testing.T) {
	chan1 := make(chan bool,1)
	chan2 := make(chan bool,1)
	writeTrue(chan1)
	select {
	case <-chan1:
		fmt.Print("chan 1")
	case <-chan2:
		fmt.Print("chan 2")
	default:
		fmt.Print("default")
	}
}
// 輸出為default, 因?yàn)閏han1和chan2都無數(shù)據(jù)可讀
func TestSelect1(t *testing.T) {
	chan1 := make(chan bool,1)
	chan2 := make(chan bool,1)
	select {
	case <-chan1:
		fmt.Print("chan 1")
	case <-chan2:
		fmt.Print("chan 2")
	default:
		fmt.Print("default")
	}
}
// 輸出為 chan 1或chan 2, 因?yàn)閏han 1 和chan 2均有可讀數(shù)據(jù)
func TestSelect2(t *testing.T) {
	chan1 := make(chan bool,1)
	chan2 := make(chan bool,1)
	writeTrue(chan1)
	writeTrue(chan2)
	select {
	case <-chan1:
		fmt.Print("chan 1")
	case <-chan2:
		fmt.Print("chan 2")
	default:
		fmt.Print("default")
	}
}

2.5 for range

對channel的for range循環(huán)可以依次從channel中讀取數(shù)據(jù),讀取數(shù)據(jù)前是不知道里面有多少元素的,如果channel中沒有元素,則會(huì)阻塞等待,直到channel被關(guān)閉,退出循環(huán)。如果代碼中沒有關(guān)閉channel的邏輯,或者插入break語句的話,就會(huì)產(chǎn)生死鎖。

func testLoopChan() {
	c := make(chan int)
	go func() {
		c <- 1
		c <- 2
		c <- 3
		time.Sleep(time.Second * 2)
		close(c)
	}()
	for x := range c {
		fmt.Printf("test:%+v\n", x)
	}
}

//結(jié)果
test:1
test:2
test:3
結(jié)束

這里需要注意,被for range輪詢過的對象可以被視為已經(jīng)從channel取出,下面我們拿兩個(gè)例子來說明:

func testLoopChan2() {
	c := make(chan int)
	go func() {
		c <- 1
		c <- 2
		c <- 3
	}()
	for x := range c {
		fmt.Printf("test:%+v\n", x)
		break
	}
	<-c
	<-c
}
//輸出
1

func testLoopChan3() {
	c := make(chan int)
	go func() {
		c <- 1
		c <- 2
		c <- 3
	}()
	for x := range c {
		fmt.Printf("test:%+v\n", x)
		break
	}
	<-c
	<-c
	<-c
}
//輸出死鎖,因?yàn)閏hannel已經(jīng)取空,最后的<-操作會(huì)導(dǎo)致阻塞

三、使用

3.1 狀態(tài)機(jī)輪轉(zhuǎn)

channel的一個(gè)核心用法就是流程控制,對于狀態(tài)機(jī)輪轉(zhuǎn)場景,channel可以輕松解決(經(jīng)典的輪流打印ABC)。

func main(){
    chanA :=make(chan struct{},1)
    chanB :=make(chan struct{},1)
    chanC :=make(chan struct{},1)
    
    chanA<- struct{}{}
    
    go printA(chanA,chanB)
    go printB(chanB,chanC)
    go printC(chanC,chanA)
}

func printA(chanA chan struct{}, chanB chan struct{}) {
    for {
        <-chanA
        println("A")
        chanB<- struct{}{}
    }
}

func printB(chanB chan struct{}, chanC chan struct{}) {
    for {
        <-chanB
        println("B")
        chanC<- struct{}{}
    }
}

func printC(chanC chan struct{}, chanA chan struct{}) {
    for {
        <-chanC
        println("C")
        chanA<- struct{}{}
    }
}

3.2 流程退出

這是我在raft實(shí)驗(yàn)中g(shù)et到的小技能,用一個(gè)channel表示是否需要退出。select中監(jiān)聽該channel,一旦被寫入,即可進(jìn)入退出邏輯

exit := make (chan bool)
//...
for {
	select {
		case <-exit:
			fmt.Print("exit code")
			return
		default:
			fmt.Print("normal code")
			//...
	}
}

3.3 超時(shí)控制

這也是我在raft實(shí)驗(yàn)中g(shù)et到的技能,如果某個(gè)任務(wù)返回,可以在該任務(wù)對應(yīng)的channel寫入,由select讀出。同時(shí)用一個(gè)case來計(jì)時(shí),如果超過該時(shí)間仍然沒有完成,則進(jìn)入超時(shí)邏輯

func control(){
	taskAChan := make (chan bool)
	TaskA(taskAChan)
	select {
		case <-taskAChan:
			fmt.Print("taskA success")
		case <- <-time.After(5 * time.Second):
			ftm.Print("timeover")
	}
}

func TaskA(taskAChan chan bool){
	//TaskA的主要代碼
	//...
	// 完成TaskA后才寫入channel
	taskAChan <- true
}

3.4 帶并發(fā)數(shù)限制的goroutine池

我實(shí)習(xí)的時(shí)候曾經(jīng)碰到一個(gè)需求,需要并發(fā)地向目標(biāo)服務(wù)器發(fā)起ftp請求,但是同一時(shí)間能發(fā)起的連接數(shù)量是有限的,需要由buffer channel對其進(jìn)行控制。該channel有點(diǎn)類似于信號量,讀取寫入會(huì)導(dǎo)致緩存空間的變化。緩存在這里起的作用類似于信號量(寫入讀取對應(yīng)PV操作),進(jìn)行任務(wù)時(shí)會(huì)寫入channel,完成任務(wù)時(shí)會(huì)讀取channel。如果緩存空間耗盡,就會(huì)新的寫入請求會(huì)阻塞,直到某一個(gè)任務(wù)完成緩存空間釋放。

var sem = make(chan int, MaxOutstanding)

func handle(r *Request) {
    sem <- 1 // 等待放行;
    process(r)
    // 可能需要一個(gè)很長的處理過程;
    <-sem // 完成,放行另一個(gè)過程。
}

func Serve(queue chan *Request) {
    for {
        req := <-queue
        go handle(req) // 無需等待 handle 完成。
    }
}

3.5 溢出緩存

在高并發(fā)環(huán)境下,為了避免請求丟失,可以選擇將來不及處理的請求緩存。這也是使用select可以實(shí)現(xiàn)的功能,如果一個(gè)buffer channel寫滿,在default邏輯中將其緩存。

func put(c message){
	select {
		case putChannel <- c:
			fmt.Print("put success")
		default:
			fmt.Print("buffer data")
			buffer(c)
	}
}

3.6 隨機(jī)概率分發(fā)

select {
        case b := <-backendMsgChan:
        if sampleRate > 0 && rand.Int31n(100) > sampleRate {
            continue
        } 
}

四、坑和經(jīng)驗(yàn)

4.1 panic

以下幾種情況會(huì)導(dǎo)致panic

  • 對nil channel進(jìn)行close
  • 對closed channel進(jìn)行close和寫(讀會(huì)讀出零值)

可以用ok值檢查channel是否為空或者關(guān)閉

queue := make(chan int, 1)

value, ok := <-queue
if !ok {
    fmt.Println("queue is closed or nil")
	queue = nil
}

4.2 關(guān)閉的channel如果使用range會(huì)提前返回

channel 關(guān)閉會(huì)導(dǎo)致range返回

4.3 對reset channel進(jìn)行寫入

如果一個(gè)結(jié)構(gòu)體的channel成員有機(jī)會(huì)被重置,它的寫入必須考慮失敗。

下面例子中,寫入跳轉(zhuǎn)到了default邏輯

type chanTest struct {
	c chan bool
}

func TestResetChannel(t *testing.T) {
	cc := chanTest{c: make(chan bool)}
	go cc.resetChan()
	select {
	case cc.c <- true:
		log.Printf("cc.c in")
	default:
		log.Printf("default")

	}
}

func (c *chanTest) resetChan() {
	c.c = make(chan bool)
}

總結(jié)

到此這篇關(guān)于golang踩坑實(shí)戰(zhàn)之channel的正確使用方式的文章就介紹到這了,更多相關(guān)golang channel的正確使用內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!

相關(guān)文章

  • GO中對map排序的實(shí)現(xiàn)

    GO中對map排序的實(shí)現(xiàn)

    本文主要介紹了GO中對map排序的實(shí)現(xiàn),文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧
    2023-03-03
  • Go語言遍歷目錄的三種方法舉例

    Go語言遍歷目錄的三種方法舉例

    學(xué)習(xí)io之后,尤其是文件操作,我們就可以遍歷給定的目錄了,這篇文章主要給大家介紹了關(guān)于Go語言遍歷目錄的三種方法,分別是ioutil.ReadDir、filepath.Walk以及filepath.Glob,需要的朋友可以參考下
    2023-11-11
  • Golang通過小程序獲取微信openid的方法示例

    Golang通過小程序獲取微信openid的方法示例

    這篇文章主要介紹了Golang通過小程序獲取微信openid的方法示例,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧
    2020-03-03
  • 完美解決go Fscanf 在讀取文件時(shí)出現(xiàn)的問題

    完美解決go Fscanf 在讀取文件時(shí)出現(xiàn)的問題

    這篇文章主要介紹了完美解決go Fscanf 在讀取文件時(shí)出現(xiàn)的問題,具有很好的參考價(jià)值,希望對大家有所幫助。一起跟隨小編過來看看吧
    2021-03-03
  • Golang中omitempty關(guān)鍵字的具體實(shí)現(xiàn)

    Golang中omitempty關(guān)鍵字的具體實(shí)現(xiàn)

    本文主要介紹了Golang中omitempty關(guān)鍵字的具體實(shí)現(xiàn),文中通過示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下
    2022-01-01
  • golang實(shí)現(xiàn)圖像驗(yàn)證碼的示例代碼

    golang實(shí)現(xiàn)圖像驗(yàn)證碼的示例代碼

    這篇文章主要為大家詳細(xì)介紹了如何利用golang實(shí)現(xiàn)簡單的圖像驗(yàn)證碼,文中的示例代碼講解詳細(xì),具有一定的學(xué)習(xí)價(jià)值,感興趣的小伙伴可以跟隨小編一起學(xué)習(xí)一下
    2023-10-10
  • Golang中的archive/zip包的常用函數(shù)詳解

    Golang中的archive/zip包的常用函數(shù)詳解

    Golang 中的 archive/zip 包用于處理 ZIP 格式的壓縮文件,提供了一系列用于創(chuàng)建、讀取和解壓縮 ZIP 格式文件的函數(shù)和類型,下面小編就來和大家講解下常用函數(shù)吧
    2023-08-08
  • 為什么Go里值為nil可以調(diào)用函數(shù)原理分析

    為什么Go里值為nil可以調(diào)用函數(shù)原理分析

    這篇文章主要為大家介紹了為什么Go里值為nil可以調(diào)用函數(shù)原理分析,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪
    2023-08-08
  • 淺析go逆向符號恢復(fù)

    淺析go逆向符號恢復(fù)

    這篇文章主要介紹了go逆向符號恢復(fù)的相關(guān)知識,本文給大家介紹的非常詳細(xì),對大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下
    2023-08-08
  • golang?gorm的Callbacks事務(wù)回滾對象操作示例

    golang?gorm的Callbacks事務(wù)回滾對象操作示例

    這篇文章主要為大家介紹了golang?gorm的Callbacks事務(wù)回滾對象操作示例,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步早日升職加薪
    2022-04-04

最新評論