Golang的channel

39 阅读2分钟

channel

单纯的将函数并发执行是没有意义的,函数与函数间需要交换数据才能提现并发执行函数的意义

虽然可以使用共享内存进行数据交换,但是共享内存在不同的goroutine中容易发生竞态问题,为了保证数据交换的正确性,必须使用互斥量对内存进行加锁,这种做法势必造成性能问题

go语言的并发模型是csp ,提倡通过通信共享内存而不是通过共享内存实现通信

如果说goroutine是go程序并发的执行体,channel就是她们之间的连接,channel是可以让一个goroutine发送特定值到另一个goroutine的通信机制

go语言中的通道channel是一种特殊的类型,通道像是一个传送带,通道像一个传送带或者队列,总是遵循先入先出的规则,保证收发数据的顺序,每一个通道都是一个具体类型的管道,也就是声明channel的时候需要为其指定元素类型

channel类型

channel是一种类型,一种引用类型,声明通道类型的格式如下

var 变量 chan 元素类型

举例:

var ch1 chan int //申明一个传递整型的通道
var ch2 chan bool // 申明一个传递布尔型的通道
var ch3 chan []int // 申明一个传递int切片的通道

创建 channel

通道是引用类型,通道类型的空置是nil

var ch chan int 
fmt.Println(ch) // nil

申明的通道后需要使用make函数初始化后才能使用

创建channel的格式如下:

make(chan 元素类型, [缓冲大小])

channel的缓冲大小是可选的

ch4 := make(chan int)
ch5 := make(chan bool)
ch6 := make(chan []int)

channel操作

通道有发送send,接收receive和关闭close三种操作

发送和接收都使用<-符号

现在我们先使用以下语句定义一个通道

ch := make(chan int)

发送

将一个值发送到通道中

ch <- 10 // 将10发送到ch中

接收

从一个通道中接收值

x := <- ch // 从ch中接收并赋值给变量x
<-ch // 从ch中接收值,忽略结果

关闭

我们通过内置的close函数来关闭通道

close(ch)

关于关闭通道需要注意的事情是,只有在通知接收方goroutine所有的数据都发送完毕的时候,才需要关闭通道,通道时可以被垃圾回收机制回收的,她和关闭文件是不一样的,在结束操作之后关闭文件是必须要做的,但关闭通道不是必须的

关闭后的通道有以下特点:

对于一个关闭的通道再发送值就会导致panic
对于一个关闭的通道进行接收一直获取值直到通道为空
对于一个关闭的并且没有值的通道执行接收会得到相应类型的零值
关闭一个已经关闭的通道会导致panic

无缓冲的通道又称为阻塞的通道

func main() {

​ ch := make(chan int)

​ ch <- 10

​ fmt.Println("发送成功")

}

上面这段代码能够通过编译,但执行时会报错

因为我们使用的是ch:=make(chan int)创建的是无缓冲的通道,无缓冲的通道只有在有人接收值的时候才能发送值,就像你住的小区没有快递柜和代收点,快递员给你打电话必须要把这个物品送到你的手中,简单来说就是无缓冲的通道必须有接收才能发送

上面的代码会阻塞在ch<-10这一行代码形成死锁,如何解决这个问题呢

一种方法是启用一个goroutine去接收值,例如

func recv(c chan int) {
    ret := <-c
    fmt.Println("接收成功", ret)
}
func main() {
    ch := make(chan int)
    go recv(ch) // 启用goroutine从通道接收值
    ch <- 10
    fmt.Println("发送成功")
}

无缓冲通道上的发送操作会阻塞,直到另一个goroutine在该通道上执行接收操作,这时值才能发送成功,两个goroutine将继续执行,相反,如果接收操作先执行, 接收方的goroutine将阻塞,知道另一个goroutine在该通道上发送一个值

使用无缓冲通道进行通信将导致发送和接收goroutine同步化,因此,无缓冲通道也被称为同步通道

有缓冲的通道

解决上面问题的方法还有一种就是使用有缓冲区的通道

我们可以在使用make函数初始化通道的时候为其指定通道的容量

func main() {
    ch := make(chan int, 1) // 创建一个容量为1 的有缓冲区的通道
    ch <- 10
    fmt.Println("发送成功")
}

只要通道的容量大于0,那么该通道就是有缓冲的通道,通道的容量表示通道中能存放元素的数量,就像你小区快递柜只有那么多个格子,格子满了就装不下,就阻塞了,等到别人取走一个,就能往里面放一个

close()

可以通过内置的close()函数关闭channel,如果你的管道不往里存值或者取值的时候一定记得关闭通道

如何优雅的从通道循环取值

当通过通道发送有限的数据时,我们可以通过close函数关闭通道来告知从该通道接收值的goroutine停止等待,当通道被关闭时,往该通道发送值会引发panic,从该通道里接收的值一直都是类型零值,那么如何判断一个通道是否被关闭了呢

func main() {
    ch1 := make(chan int)
    ch2 := make(chan int)
    // 开启goroutine将0~100的书发送到ch1中
    go func() {
        for i := 0; i< 100; i ++ {
            ch1 <- i
        }
        close(ch1)
    }()
    
    // 开启goroutine从ch1中接收值,并将该值的平方发送到ch2中
    go func() {
        for {
            i, ok := <- ch1 //通道关闭后再取值ok=false
            if !ok{
                break
            }
            ch2 <- i*i
        }
        close(ch2)
    }()
    // 在主goroutine中从ch2中接收值打印
    for i := range ch2 { // 通道关闭后会退出for range 循环
        fmt.Println(i)
    }
}

从上面的例子中我们看到有两种方式在接收值的时候判断通道是否被关闭,我们通常使用的是for range 方式

单向通道

有的时候我们会将通道作为参数在多个任务函数间传递,很多时候我们在不同的任务函数中使用通道都会对其进行限制,比如限制通道再函数中只能发送或只能接收

本文由博客群发一文多发等运营工具平台 OpenWrite 发布