15.GO-管道

董雷發表於2022-01-04

管道

管道是經常被設計的概念,特別是C語言,在進場間通訊中非常常用,在Go中更多用在協程間通訊

在多個goroutine併發中,我們不僅可以通過原子函式和互斥鎖保證對共享資源的安全訪問,消除競爭的狀態,還可以通過使用管道,在多個goroutine傳送和接受共享的資料,達到資料同步的目的。

管道,他有點像在兩個routine之間架設的管道,一個goroutine可以往這個管道里塞資料,另外一個可以從這個管道里取資料,有點類似於我們說的佇列。

//宣告一個通道很簡單,我們使用chan關鍵字即可,除此之外,還要指定通道中傳送和接收資料的型別,這樣我們才能知道,要傳送什麼型別的資料給通道,也知道從這個通道里可以接收到什麼型別的資料。
//通道型別和Map這些型別一樣,可以使用內建的make函式宣告初始化,這裡我們初始化了一個chan int型別的通道,所以我們只能往這個通道里傳送int型別的資料,當然接收也只能是int型別的資料。
ch:=make(chan int)

管道使用

//看例子,慢慢理解傳送和接收的用法。傳送操作<-在通道的後面,看箭頭方向,表示把數值2傳送到通道ch裡;接收操作<-在通道的前面,而且是一個一元操作符,看箭頭方向,表示從通道ch裡讀取資料。讀取的資料可以賦值給一個變數,也可以忽略。
ch <- 2 //傳送數值2給這個通道
x:=<-ch //從通道里讀取值,並把讀取的值賦值給x變數
<-ch //從通道里讀取值,然後忽略

通道我們還可以使用內建的close函式關閉。

func main() {
      ch := make(chan int)
      //如果一個通道被關閉了,我們就不能往這個通道里傳送資料了,如果傳送的話,會引起painc異常。但是,我們還可以接收通道里的資料,如果通道里沒有資料的話,接收的資料是零值。
     close(ch)
     ch <- 1
   }
 //列印結果  
///private/var/folders/w9/qkyh3gtx0jvdphr97dvpndyw0000gn/T/___go_build_test2_go
//panic: send on closed channel
//goroutine 1 [running]:

無緩衝的管道

無緩衝的通道指的是通道的大小為0,也就是說,這種型別的通道在接收前沒有能力儲存任何值,它要求傳送goroutine和接收goroutine同時準備好,才可以完成傳送和接收操作。

從上面無緩衝的通道定義來看,傳送goroutine和接收gouroutine必須是同步的,同時準備後,如果沒有同時準備好的話,先執行的操作就會阻塞等待,直到另一個相對應的操作準備好為止。這種無緩衝的通道我們也稱之為同步通道。

ch:=make(chan int)
ch:=make(chan int,0)

無緩衝管道案例

//在計算sum和的goroutine沒有執行完,把值賦給ch通道之前,fmt.Println(<-ch)會一直等待,所以main主goroutine就不會終止,只有當計算和的goroutine完成後,並且傳送到ch通道的操作準備好後,同時<-ch就會接收計算好的值,然後列印出來。
func main() {
    ch := make(chan int)
    go func() {
        fmt.Println("協程開始處理!")
        var sum int = 0
        for i := 0; i < 10; i++ {
            sum += i
        }
        fmt.Println(0)
        time.Sleep(time.Second * 4)
        fmt.Println("開始往管道插入資料")
        ch <- sum
        fmt.Println(11111)
    }()
    fmt.Println(2222)
    //<-ch 會被阻塞,知道  ch <- sum 這個操作完成,否則會一直阻塞
    fmt.Println(<-ch)
    fmt.Println(33333)
}
//返回資料
2222
協程開始處理!
0
開始往管道插入資料
11111
45
33333

有緩衝的通道

有緩衝通道,其實是一個佇列,這個佇列的最大容量就是我們使用make函式建立通道時,通過第二個引數指定的。

//這裡建立容量為3的,有緩衝的通道。對於有緩衝的通道,向其傳送操作就是向佇列的尾部插入元素,接收操作則是從佇列的頭部刪除元素,並返回這個剛剛刪除的元素。
ch := make(chan int, 3)

當佇列滿的時候,傳送操作會阻塞;當佇列空的時候,接受操作會阻塞。有緩衝的通道,不要求傳送和接收操作時同步的,相反可以解耦傳送和接收操作。

//想知道通道的容量以及裡面有幾個元素資料怎麼辦?其實和map一樣,使用cap和len函式就可以了
cap(ch)
len(ch)

管道容量無資料時

func main() {
    ch := make(chan int,1)
    go func() {
        fmt.Println("協程開始處理!")
        var sum int = 45
        fmt.Println(0)
        time.Sleep(time.Second * 4)
        fmt.Println("開始往管道插入資料")
        ch <- sum
        fmt.Println(11111)
    }()
    fmt.Println(2222)
    //雖然管道容量是1,但是因為管道里面沒資料,所以還是會阻塞的,一直等到  ch <- sum 寫入資料才會執行  <-ch
    fmt.Println(<-ch)
    fmt.Println(33333)
}
// 返回
2222
協程開始處理!
0
開始往管道插入資料
11111
45
33333

管道有資料時

func main() {
    ch := make(chan int,1)
    ch <- 78
    go func() {
        fmt.Println("協程開始處理!")
        var sum int = 45
        fmt.Println(0)
        time.Sleep(time.Second * 4)
        fmt.Println("開始往管道插入資料")
        ch <- sum
        fmt.Println(11111)
    }()
    fmt.Println(2222)
    //因為管道中有資料,所以不需要等待 ch <- sum 操作,也就不需要阻塞了
    fmt.Println(<-ch)
    fmt.Println(33333)
}
//結果
2222
78
33333

管道插入的資料超過容量

func main() {
    ch := make(chan int,1)
    ch <- 78
    //管道插入的資料不能超過容量本身,否者會報錯的
    ch <- 56
}
//返回
fatal error: all goroutines are asleep - deadlock!

單行管道

有時候,我們有一些特殊場景,比如限制一個通道只可以接收,但是不能傳送;有時候限制一個通道只能傳送,但是不能接收,這種通道我們稱為單向通道(半雙工管道)。

定義單向通道

//只能傳送
var send chan<- int 
//只能接收
var receive <-chan int 

單向通道應用於函式或者方法的引數比較多

//例子這樣的,只能進行傳送操作,防止誤操作,使用了接收操作,如果使用了接收操作,在編譯的時候就會報錯的。
func counter(out chan<- int) {
}

額外注意

案例1.沒有協程的場景

    c2 := make(chan os.Signal,1)
    //這是因為在“<-c2”處,期望從管道中拿到資料,而這個資料必須是其他goroutine協程放入管道的。但是這裡並沒有其他goroutine,那麼就永遠不會有資料放入管道。所以,main goroutine線在等一個永遠不會來的資料,那整個程式就永遠等下去了,所以程式down掉。
    s2 := <-c2
    fmt.Println("退出訊號", s2)
    os.Exit(200)

//返回
fatal error: all goroutines are asleep - deadlock!

案例2.main有寫入資料時

c1 := make(chan os.Signal,1)
    c1 <- syscall.SIGINT
    s1 := <-c1
    fmt.Println("退出訊號", s1)
    os.Exit(200)
//返回
退出訊號 interrupt

案例3

    c := make(chan os.Signal,1)
    //當執行“go func”時,協程一直在迴圈處理,一直有協程在工作,通道一直在等待協程往裡放資料,所有不會down掉。
    go func() {
        time.Sleep(time.Second * 5)
        fmt.Println("開始執行 syscall.SIGINT")
        c <- syscall.SIGINT
        fmt.Println("結束執行 syscall.SIGINT")
    }()
    s := <-c
    fmt.Println("退出訊號", s)
    os.Exit(200)
//返回
開始執行 syscall.SIGINT
結束執行 syscall.SIGINT
退出訊號 interrupt

案例4

    c2 := make(chan os.Signal,1)
    //監聽所有訊號,main有這個函式
    signal.Notify(c2)
    //main有Notify這個函式,那麼 <-c2 就會阻塞
    s2 := <-c2
    fmt.Println("退出訊號", s2)
    os.Exit(200)
//返回
^C退出訊號 interrupt

案例5

    ints := make(chan string,3)
    go func() {
        fmt.Println(1111)
        //因為管道里面沒有資料,所以這裡會一直阻塞,不會執行 fmt.Println("aaa" + a)
        a := <-ints
        fmt.Println("aaa" + a)
    }()
    time.Sleep(time.Second * 3)
    fmt.Println(333333)
    os.Exit(2000)
//返回
1111
333333
本作品採用《CC 協議》,轉載必須註明作者和本文連結
good good study day day up

相關文章