[golang] channel通道

Posted liujunhang

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了[golang] channel通道相关的知识,希望对你有一定的参考价值。

说明

channel是go当中的一个核心类型,可以看做是管道。并发核心单元可以通过channel进行数据的发送和接收,从而实现通信。

在go中,channel是一种数据类型,主要被用来解决协程的同步问题以及协程之间数据共享(数据传递)的问题。

go当中的goroutine运行在相同的地址空间,因此访问共享内存地址必须做好同步,goroutine奉行通过通信来共享内存,而不是共享内存来通信。

引用类型channel可用于多个goroutine通讯,在其内部实现了同步,确保并发安全。

技术图片

定义channel变量

在go中,channel也一个对应make创建的底层数据结构的引用

当我们复制一个channel或者用于函数参数传递的时候,我们只是拷贝了一个channel引用,因此调用者和被调用者将引用同一个channel对象,和其他的引用类型一样,channel的零值也是nil。

定义一个channel的时候,也需要定义发送到channel的值的类型,channel可以使用内置的make函数来进行创建。

chan是创建channel使用的关键字,Type代表着channel收发数据的类型。

make(chan Type) // 等价于make(chan Type,0)
make(chan Type,capacity)

当 参数capacity= 0 时,channel 是无缓冲阻塞读写的;当capacity > 0 时,channel 有缓冲、是非阻塞的,直到写满 capacity个元素才阻塞写入。

channel非常像生活中的管道,一边可以存放东西,另一边可以取出东西。channel通过操作符 <- 来接收和发送数据,发送和接收数据语法:

package main

import "fmt"

func main() {
    // 创建一个channel
    ch := make(chan int)
    // 创建一个子goroutine,在这个goroutine中进行数据的写入
    go worker(ch)
    // 读取channel通道中的数据
    num := <- ch
    fmt.Println("channel通道中读到的数据为:",num)
}

func worker(ch chan int) {
    // 向channel通道中写入数据
    ch <- 10
}

在上面的代码中,我们通过chan <- val 来进行channel值的写入,通过val := <- chan 来进行channel的值的读取。

我们首先创建了一个goroutine用来向channel管道中进行数据的写入,然后同时在main goroutine中进行数据的读取。

输出的结果为:

channel通道中读到的数据为: 10

Process finished with exit code 0

如果channel(无缓冲)的一头发生了读操作或者写操作,而在另外一头没有进行相对应的操作,那么就会发生阻塞,直到channel的另一头发生相应的操作。

默认情况下,channel接收和发送数据都是阻塞的,除非另一端已经准备好,这样就使得goroutine同步变的更加的简单,而不需要显式的lock。

package main

import (
    "fmt"
)

func main() {
????c := make(chan int)

????go func() {
????????defer fmt.Println("子协程结束")

????????fmt.Println("子协程正在运行……")

????????c <- 666 //666发送到c
????}()

????num := <-c //从c中接收数据,并赋值给num

????fmt.Println("num = ", num)
????fmt.Println("main协程结束")
}

程序运行结果:

技术图片

当进行读取数据的时候,可以采用下面的形式来判断数据是否传输完毕:

?x, ok := <-channel?

如果数据传输完毕,ok的值为false,否则为true。

package main

import "fmt"

func main() {
    // 创建一个channel
    ch := make(chan int)

    // 创建一个goroutine用来向channel中写入数据
    go func() {
        defer close(ch) // 通过close函数可以关闭channel通道
        for i:=0;i<5;i++ {
            ch <- i // 将i值写入channel
        }
    }()

    // 在主goroutine中读取channel通道传递的值
    for {
        val,ok := <-ch
        if !ok { // ok 为false时表示读取结束
            fmt.Println("读取结束,over")
            break
        }
        fmt.Println("读取到的数据为:",val)
    }
}

输出的结果为:

读取到的数据为: 0
读取到的数据为: 1
读取到的数据为: 2
读取到的数据为: 3
读取到的数据为: 4
读取结束,over

Debugger finished with exit code 0

定义无缓冲的channel

无缓冲的通道(unbuffered channel)是指在接收前没有能力保存任何值的通道。

这种类型的通道要求发送goroutine和接收goroutine同时准备好,才能完成发送和接收操作。否则,通道会导致先执行发送或接收操作的 goroutine 阻塞等待。

这种对通道进行发送和接收的交互行为本身就是同步的。其中任意一个操作都无法离开另一个操作单独存在。

例如上面我们写的程序其实都是无缓冲的channel。

阻塞:由于某种原因数据没有到达,当前协程(线程)持续处于等待状态,直到条件满足,才接触阻塞。

同步:在两个或多个协程(线程)间,保持数据内容一致性的机制。

下图展示两个 goroutine 如何利用无缓冲的通道来共享一个值:

技术图片

定义有缓冲的channel

有缓冲的通道(buffered channel)是一种在被接收前能存储一个或者多个数据值的通道。

这种类型的通道并不强制要求 goroutine 之间必须同时完成发送和接收。通道会阻塞发送和接收动作的条件也不同。

只有通道中没有要接收的值时,接收动作才会阻塞。

只有通道没有可用缓冲区容纳被发送的值时,发送动作才会阻塞。

这导致有缓冲的通道和无缓冲的通道之间的一个很大的不同:无缓冲的通道保证进行发送和接收的 goroutine 会在同一时间进行数据交换;有缓冲的通道没有这种保证。

示例图如下:

技术图片

有缓冲的channel创建格式:

make(chan Type, capacity)

如果给定了一个缓冲区容量,通道就是异步的。只要缓冲区有未使用空间用于发送数据,或还包含可以接收的数据,那么其通信就会无阻塞地进行。

package main

import "fmt"

func main() {
    // 定义一个有缓冲的channel
    ch := make(chan int,2)

    // 创建一个goroutine,用来写入数据
    go func() {
        defer close(ch)
        for i:=0;i<5;i++ {
            ch <- i
            fmt.Println("写入的数据为:",i) // print操作属于io操作,可能导致cpu时间片切换
        }
    }()

    for {
        num,ok := <- ch
        if !ok {
            fmt.Println("读取结束,over")
            break
        }
        fmt.Println("读取到的数据为:",num)
    }
}

运行的结果为:

写入的数据为: 0
读取到的数据为: 0
读取到的数据为: 1
写入的数据为: 1
写入的数据为: 2
写入的数据为: 3
写入的数据为: 4
读取到的数据为: 2
读取到的数据为: 3
读取到的数据为: 4
读取结束,over

Process finished with exit code 0

关闭channel

如果发送者知道,没有更多的值需要发送到channel的话,那么让接收者也能及时知道没有多余的值可接收将是有用的,因为接收者可以停止不必要的接收等待。这可以通过内置的close函数来关闭channel实现。

package main

import (
    "fmt"
)

func main() {
????c := make(chan int)

????go func() {
????????for i := 0; i < 5; i++ {
????????????c <- i
????????}
????????//把 close(c) 注释掉,程序会一直阻塞在 if data, ok := <-c; ok 那一行
????????close(c)
????}()

????for {
????????//ok为true说明channel没有关闭,为false说明管道已经关闭
????????if data, ok := <-c; ok {
????????????fmt.Println(data)
????????} else {
????????????break
????????}
????}

????fmt.Println("Finished")
}

程序运行结果:

技术图片

注意:

  • channel不像文件一样需要经常去关闭,只有当你确实没有任何发送数据了,或者你想显式的结束range循环之类的,才去关闭channel;

  • 关闭channel后,无法向channel 再发送数据(引发 panic 错误后导致接收立即返回零值);
  • 关闭channel后,可以继续从channel接收数据;
  • 对于nil channel,无论收发都会被阻塞。

可以使用 range 来迭代不断操作channel:

package main

import (
    "fmt"
)

func main() {
????c := make(chan int)

????go func() {
????????for i := 0; i < 5; i++ {
????????????c <- i
????????}
????????//把 close(c) 注释掉,程序会一直阻塞在 for data := range c 那一行
????????close(c)
????}()

????for data := range c {
????????fmt.Println(data)
????}
????fmt.Println("Finished")
}

单向channel

默认情况下,通道channel是双向的,也就是,既可以往里面发送数据也可以同里面接收数据。

但是,我们经常见一个通道作为参数进行传递而值希望对方是单向使用的,要么只让它发送数据,要么只让它接收数据,这时候我们可以指定通道的方向。

技术图片

单向channel变量的声明非常简单,如下:

var ch1 chan int       // ch1是一个正常的channel,是双向的
var ch2 chan<- float64 // ch2是单向channel,只用于写float64数据
var ch3 <-chan int     // ch3是单向channel,只用于读int数据
//   chan<- //只写
func counter(out chan<- int) {
????defer close(out)
????for i := 0; i < 5; i++ {
????????out <- i //如果对方不读 会阻塞
????}
}

//   <-chan //只读
func printer(in <-chan int) {
????for num := range in {
????????fmt.Println(num)
????}
}

func main() {
????c := make(chan int) //   chan   //读写

????go counter(c) //生产者
????printer(c)    //消费者

????fmt.Println("done")
}

以上是关于[golang] channel通道的主要内容,如果未能解决你的问题,请参考以下文章

[golang] channel通道

golang 如何查看channel通道中未读数据的长度

Go语言自学系列 | golang并发变成之通道channel

Golang中的channel代码示例----无缓冲有缓冲rangeclose

GoLang协程与通道---上

协程与Channels (CSP: Kotlin, Golang)