• [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")
    }
    
  • 相关阅读:
    Cogs 465. 挤牛奶
    洛谷P1083 借教室
    Cogs 1264. [NOIP2012] 开车旅行(70分 暴力)
    2017-10-19 NOIP模拟赛
    Codevs 2144 砝码称重 2
    洛谷P1450 [HAOI2008]硬币购物
    洛谷P2534 [AHOI2012]铁盘整理
    洛谷P1731 生日蛋糕
    2017-10-18 NOIP模拟赛
    洛谷P1074 靶形数独
  • 原文地址:https://www.cnblogs.com/liujunhang/p/12536331.html
Copyright © 2020-2023  润新知