Go语言之Channel,你用过吗?

搁浅 2023-01-31 11:05:25 1971

ChannelGolang语言的特色,用于多个Goroutine之间的通信。本文首先探讨对其发送、接收操作的特点,然后讨论什么时候channel会阻塞、什么时候channelpanic,最后讨论专门为channel而生的select语句

channel通道发送与接收操作的特点

  • 发送操作与接收操作都是互斥的

解释:多个goroutine中同时向同一个channel发送或者接收时,只能一个goroutinechannel进行发送或接收,发送之间互斥,接收之间也互斥。 发送操作在完全完成之前会被阻塞,接收操作也是如此。 发送操作和接收操作中对元素值的处理都是不可分割的。 解释: 发送操作,不是直接将值将入到channel,而是第一步、进行复制,第二步、将副本加入到channel中,这个过程不可分割且会阻塞。 接收操作,不是直接将值直接赋值给变量,而是第一步、进行复制,第二步、将副本赋值给变量,第三部、删除channel中的相应元素。这个过程不可分割且会阻塞。

对通道的发送与接收操作,什么时候会阻塞?

  • 通道为nil时,发送与接收操作全都会阻塞

解释:通道channel为引用类型,只声明不用make初始化,此时channel就是一个nil

package main

import (
    "fmt"
    "time"
)

func main()  {
    var ch chan int
    go func() {
        ch <- 1
        fmt.Println("goroutine finish")
    }()
    time.Sleep(10*time.Second)
}
// main goroutine 会等待10s,不会打印goroutine finish,说明一直阻塞
  • 缓冲通道,已满,进行发送操作,发送操作阻塞
package main

import (
    "fmt"
    "time"
)

func main() {
    var ch = make(chan int, 3)
    ch <- 1
    ch <- 2
    ch <- 3
    fmt.Println("channel is fill")
    go func() {
        ch <- 4
        fmt.Println("fill channel can  receive a number?")
    }()
    time.Sleep(5 * time.Second)
}
  • 缓冲通道,已空,进行接收操作,接收操作阻塞
  • 非缓冲通道,只有当发送和接收都准备好(其实就是同步),才不会阻塞。
package main

func main() {
    // 示例1。
    ch1 := make(chan int, 1)
    ch1 <- 1
    //ch1 <- 2 // 通道已满,因此这里会造成阻塞。

    // 示例2。
    ch2 := make(chan int, 1)
    //elem, ok := <-ch2 // 通道已空,因此这里会造成阻塞。
    //_, _ = elem, ok
    ch2 <- 1

    // 示例3。
    var ch3 chan int
    //ch3 <- 1 // 通道的值为nil,因此这里会造成永久的阻塞!
    //<-ch3 // 通道的值为nil,因此这里会造成永久的阻塞!
    _ = ch3
}

通道的接收与发送什么时候会引发panic

一句话总结:跟通道channel关闭有关 * 通道已经关闭,但是依然向其发送元素

package main

func main()  {
    var ch = make(chan int, 2)
    close(ch)
    ch <- 1
}
  • 再次关闭已经关闭的通道
package main

func main()  {
    var ch = make(chan int, 2)
    close(ch)
    //ch <- 1
    close(ch)
}

一个正常的收发channel示例

package main

import "fmt"

func main() {
    var ch = make(chan int, 2)

    go func() {
        for i := 0; i < 10; i++ {
            fmt.Println("sender channel a val: ", i)
            ch <- i
        }
        fmt.Println("sender finished, will close the channel")
        close(ch)
    }()

    for {
        elem, ok := <- ch
        if !ok {
            fmt.Println("channel has already closed.")
            break
        } else {
            fmt.Println("receive a val from channel, ", elem)
        }
    }
    fmt.Println("End.")
}

select

基础概念

select语句是专门为了通道channel而设计的,所以select的每个case表达式中,都只能包含操作通道的表达式,比如接收表达式和发送表达式

select本质上就是对通道channelI/O操作的监听器。

// 给定几个通道,哪个通道不为空,便执行相应语句

// 准备好几个通道。
intChannels := [3]chan int{
  make(chan int, 1),
  make(chan int, 1),
  make(chan int, 1),
}
// 随机选择一个通道,并向它发送元素值。
index := rand.Intn(3)
fmt.Printf("The index: %d\n", index)
intChannels[index] <- index
// 哪一个通道中有可取的元素值,哪个对应的分支就会被执行。
select {
case <-intChannels[0]:
  fmt.Println("The first candidate case is selected.")
case <-intChannels[1]:
  fmt.Println("The second candidate case is selected.")
case elem := <-intChannels[2]:
  fmt.Printf("The third candidate case is selected, the element is %d.\n", elem)
default:
  fmt.Println("No candidate case is selected!")
}

注意点

  • selectcase分支必须是关于通道的发送表达式或者接收表达式。
  • 如果select有默认的default分支,那么无论case条件中对通道的操作是否阻塞,select语句是不会阻塞的。
  • select语句中只能有一个default分支,且分支的位置不重要。都是evaluated各个case分支之后且不满足的时候才会执行default分支的语句。
  • 如果select没有默认的default分支,而且case条件中对通道的操作都阻塞,select语句会阻塞的。如果发生阻塞,就认为该case表达式不满足执行条件,进行下一个case分支的判断。
  • 一个select语句只能对其中的每个case表达式各求值一次,如果想要迭代求值,需要for语句的帮助。但是请注意,如果此时select中的case语句当中有break执行,那么并不会跳出外层的for语句,而是会继续循环。
  • 如果select语句发现同时有多个case分支满足选择条件,那么它就会用一种伪随机的算法在这些分支中选择一个执行。注意,即使select语句是在被唤醒时发现的这种的情况,也会这么做。
func example2() {
    intChan := make(chan int, 1)
    // 一秒后关闭通道。
    time.AfterFunc(time.Second, func() {
        close(intChan)
    })
    select {
    case _, ok := <-intChan:
        if !ok {
            fmt.Println("The candidate case is closed.")
            break
        }
        fmt.Println("The candidate case is selected.")
    }
}
// result
The candidate case is closed.

程序分析,代码运行到select的唯一case分支时,因为此时intChan为空,无法进行接收,所以阻塞,1s之后,通道关闭,二元接收值中的okfalse,所以执行答应The candidate case is closed.

再次看一个示例,到底哪个case被执行,或者default被执行

package main

import "fmt"

var channels = [3]chan int{
    nil,
    make(chan int),
    nil,
}

var numbers = []int{0, 1, 2}

func main() {
    select {
    case getChan(0) <- getNumber(0):
        fmt.Println("The first candidate case is selected.")
    case getChan(1) <- getNumber(1):
        fmt.Println("The second candidate case is selected.")
    case getChan(2) <- getNumber(2):
        fmt.Println("The third candidate case is selected")
    default:
        fmt.Println("No candidate case is selected!")
    }
}

func getNumber(i int) int {
    fmt.Printf("numbers[%d]\n", i)
    return numbers[i]
}

func getChan(i int) chan int {
    fmt.Printf("channels[%d]\n", i)
    return channels[i]
}

// result
No candidate case is selected!

为何? 因为三个case分支全都阻塞 第一、第三:试图向nil通道发送数据,阻塞之 第二个试图向无缓冲通道发送,因为没有相应的接收操作,阻塞之 所以只能执行default分支的语句

如果想要第二个case分支运行,只需要做如下修改

package main

import "fmt"

var channels = [3]chan int{
    nil,
    make(chan int),
    nil,
}

var numbers = []int{0, 1, 2}

func main() {
    go func() {
        for {
            <- getChan(1)
        }
    }()
    select {
    case getChan(0) <- getNumber(0):
        fmt.Println("The first candidate case is selected.")
    case getChan(1) <- getNumber(1):
        fmt.Println("The second candidate case is selected.")
    case getChan(2) <- getNumber(2):
        fmt.Println("The third candidate case is selected")
    default:
        fmt.Println("No candidate case is selected!")
    }
    fmt.Println("finish")
}

func getNumber(i int) int {
    fmt.Printf("numbers[%d]\n", i)
    return numbers[i]
}

func getChan(i int) chan int {
    fmt.Printf("channels[%d]\n", i)
    return channels[i]
}

起一个goroutine,接收无缓冲通道的元素

参考资料

1、极客时间《Go语言核心36讲》

原文:zhuanlan.zhihu.com/p/375934691

这家伙太懒了,什么也没留下。

社区声明 1、本站提供的一切软件、教程和内容信息仅限用于学习和研究目的
2、本站资源为用户分享,如有侵权请邮件与我们联系处理敬请谅解!
3、本站信息来自网络,版权争议与本站无关。您必须在下载后的24小时之内,从您的电脑或手机中彻底删除上述内容
最新回复 (0)

您可以在 登录 or 注册 后,对此帖发表评论!

返回