go语言基础-----17-----channel创建、读写、安全关闭、多路复用select

1 通道channel介绍

  • 1)channel,可译为通道,是go语言协程goroutine之间的通信方式。
  • 2)channel通信可以想象成从管道的一头塞进数据,从另一头读取数据。通道作为容器是有限定大小的,满了就写不进去,空了就读不出来。
  • 3)channel是拥有数据类型的,channel只能传递指定的数据类型的值。
  • 4)多协程操作时(即多个写多个读),它是协程安全的,不需要额外加锁。

2 创建通道

创建通道只有一种语法,使用make 函数。
创建的通道有两种通道类型:

  • 1)缓冲型通道。
//语法:
c := make(chan 数据类型, 通道大小)
// 例如:
c := make(chan int, 1024)
  • 2)非缓冲型通道。
//语法:
c := make(chan 数据类型)
// 例如:
c := make(chan int)

有无缓存的区别在于是否有固定大小的缓存,并且如果这个缓存型通道的大小足够大,那么写操作时永远不会阻塞。

3 读写通道操作

  • 1)Go 语言为通道的读写设计了特殊的箭头语法糖 <-,让我们使用通道时非常方便。把箭头写在通道变量的右边就是写通道,把箭头写在通道的左边就是读通道。一次只能读写一个元素.
  • 2)通道作为容器,它可以像切片一样,使用 cap() 和 len() 全局函数获得通道的容量和当前内部的元素个数。

例如:

ch := make(chan float32, 4)
for i := 0; i < cap(ch); i++ {
	ch <- 1.0 // 写通道
}
for len(ch) > 0 {
	value := <-ch // 读通道
	fmt.Println(value)
}

4 读写阻塞

  • 1)通道为空,读操作会阻塞。
  • 2)通道为满,写操作会阻塞。协程就会进入休眠,直到有其它协程读通道挪出了空间,协程才会被唤醒。如果有多个协程的写操作都阻塞了,一个读操作只会唤醒一个协程。

以下两个例子中,任何一个都能说明通道为空时,读操作阻塞,通道为满,写操作阻塞的特点,可以任选看一个,有时间最好两个都看吧,加深印象。

例子1:

package main

import (
	"fmt"
	"time"
)

func main() {

	// ch1 := make(chan int, 1)                          // 这里是缓存 有一个1元素
	// fmt.Println("len: ", len(ch1), "cap: ", cap(ch1)) // output: len:  0 cap:  1
	ch2 := make(chan int)                             // 非缓存的,实际是0个,并不是1个
	fmt.Println("len: ", len(ch2), "cap: ", cap(ch2)) // output: len:  0 cap:  0

	go func() {
		time.Sleep(1 * time.Second)
		fmt.Println("开始写第一个")
		ch2 <- 1
		fmt.Println("写第一个完毕")

		fmt.Println("开始写第二个")
		ch2 <- 1 // 若第一个还没有读完,这里开始写第二个,那么这里会阻塞,下面的语句不会打印
		fmt.Println("写第二个完毕")
	}()

	fmt.Println("开始读第一个")
	value1 := <-ch2 // 因为睡眠了1s,没有写入任何内容,即通道为空,所以这里会阻塞。
	fmt.Println("读第一个完毕")

	fmt.Println("开始读第二个")
	value1 = <-ch2 // 假设第一个的读写都执行完毕,那么:若这里先执行,而第二个写没执行,那么这里先阻塞;若第二个写先执行,那么这里就不会阻塞。
	fmt.Println("读第二个完毕")

	time.Sleep(5 * time.Second)
	fmt.Println("退出, value1:", value1)
}

对打印结果进行分析:

  • 1)首先因为写协程睡了1s,此时通道为空,所以读协程先打印第二行,并阻塞在value1 := <-ch2。
  • 2)然后写协程睡醒,打印第3行,并往通道写入了一个数据。
  • 3)第四行、第五行的打印比第六行优先打印,有可能是因为此时已经读取完毕,但在输出IO中,CPU优先打印的结果;或者根本就没有读取完等待打印。这两种可能都是存在的。
  • 4)然后读线程往下执行,打印了“开始读第二个“。此时读线程可能阻塞或者不阻塞,看value1 = <-ch2语句的注释。
  • 5)最后倒数第三行与倒数第二行的顺序无关紧要了。因为可能因IO输出而改变,或者因读写完后,各自协程往下执行的速度快慢而改变。所以这个顺序不需要理会。
  • 6)最后一行是程序退出的打印结果,不必关系。
    在这里插入图片描述

例子2:

package main

import (
	"fmt"
	"math/rand"
	"time"
)

func send(ch chan int) {
	for {
		var value = rand.Intn(100)
		fmt.Println("www")
		ch <- value // 因为通道大小为1,若读协程没有读完,那么写下一个时必定会阻塞。自己可以在读前后、写前后添加打印看。
		//但注意可能因为输出IO导致读写后的后面的那句打印顺序不太准确,但是读写前的那个打印语句是没问题的(例如www与rrr)
		fmt.Printf("send %d\n", value) // 这里没有延时
	}
}

func recv(ch chan int) {
	for {
		fmt.Println("rrr")
		value := <-ch
		fmt.Printf("recv %d\n", value)
		time.Sleep(time.Second)
	}
}

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

	// 子协程循环读
	go recv(ch)

	time.Sleep(3 * time.Second) // 睡多几s,验证通道为空时,子线程读会阻塞。

	// 主协程循环写
	send(ch)
}

我们抽出部分打印进行分析:

  • 1)首先因为main睡了3s,所以子协程必定先执行,因为此时没有东西读,所以子协程阻塞在value := <-ch语句。所以打印了rrr。
  • 2)然后睡眠唤醒,主协程执行,所以打印了www,并且写协程抢CPU厉害,所以打印了send 81。此时通道有81这一个数据。
  • 3)此时来到下图的第四行,因为写协程没有睡眠操作,而读协程有,所以写协程的抢占CPU能力强,所以打印了www。
  • 4)然后有人会问,那为什么第五行打印了send 87呢?通道不是有一个数据了吗,怎么还能写入,实际上此时已经读取81这个数据完毕,只不过因为输出IO缓冲时,CPU先打印了第五行,而第7行后被打印而已。 那么看懂这个后,第6行的www打印就简单了,因为87是第二个数据,并且没有被读取,所以打印www后就阻塞住了。

这是下图的第一个读写的流程分析,剩余的同理。
在这里插入图片描述

5 关闭通道

  • 1)关闭通道后,读取值能读取到写入通道时的值,若读取完写入时的值后,则会返回通道类型的[零值],程序不会崩溃。所以如果通道里的元素是整型的,读操作是不能通过返回值来确定通道是否关闭的。
  • 2)关闭通道后,写入值会抛异常,程序崩溃。
// 2.4 关闭通道
/*
Go 语言的通道有点像文件,不但支持读写操作, 还支持关闭。
读取一个已经关闭的通道会立即返回通道类型的「零值」,而写一个已经关闭的通道会抛异常。
如果通道里的元素是整型的,读操作是不能通过返回值来确定通道是否关闭的。
*/
package main

import "fmt"

func main() {
	var ch = make(chan int, 4)
	ch <- 11
	ch <- 12

	fmt.Println("len(ch):", len(ch), "cap(ch):", cap(ch))
	close(ch) // 关闭通道

	// 关闭通道后,读取值能读取到写入通道时的值,若读取完写入时的值后,则会返回通道类型的[零值],程序不会崩溃。
	value := <-ch
	fmt.Println(value)
	value = <-ch
	fmt.Println(value)
	value = <-ch
	fmt.Println(value)
	value = <-ch
	fmt.Println(value)

	value = <-ch // 即使第五次读取超过通过的容量大小,也不会程序崩溃。
	fmt.Println(value)

	// 关闭通道后,写入值会抛异常,程序崩溃。
	ch <- 3
}

结果:
在这里插入图片描述

6 通道写安全

那如何安全关闭呢?下面是针对于一写多读的安全关闭方法,主要思想是:

  • 1)因为只有一个写的协程,那么我们只需要在写协程去关闭通道即可,不要在读协程关闭通道,这样可以做到通道写安全。但是这种场景并不是很常见,其它场景的通用写法需要继续往下看。
// 2-5 通道写安全
package main

import "fmt"

func send(ch chan int) { // 在写入端关闭,能确保写安全,但没有太多的通用性
	ch <- 1
	ch <- 2
	ch <- 3
	ch <- 4
	close(ch)
}

func recv(ch chan int) {
	for v := range ch { // 遍历的时候,若通道没有数据,会阻塞等待。
		fmt.Println(v)
	}

	// 取通道数据最好使用遍历,因为下面单独取数据是无法判断是否读取通道数据完毕。
	value := <-ch
	fmt.Println("value:", value)
}

// 确保通道写安全的最好方式是由负责写通道的协程自己来关闭通道,读通道的协程不要去关闭通道。
func main() {
	var ch = make(chan int, 1)
	go send(ch)
	recv(ch)
}

结果:
在这里插入图片描述

如果我们把下面两行注掉:

ch <- 3
ch <- 4

结果为:
在这里插入图片描述

说明for循环遍历时,只会读取写入时的数据的长度。

7 WaitGroup

上面提到,这种只是针对一写多读的通道写安全,那么多个写入怎么办呢?
可以使用go提供给我们的内置 sync 包里面的 WaitGroup 对象,它使用计数来等待指定事件完成。

// 2-6 WaitGroup 在写端关闭channel对单写的程序有效,但是多写的时候呢?

package main

import (
	"fmt"
	"sync"
	"time"
)

func send(ch chan int, wg *sync.WaitGroup) {
	defer wg.Done() // 写完时,计数值减一

	i := 0
	for i < 4 {
		i++
		ch <- i
	}
}

func recv(ch chan int) {
	for v := range ch {
		fmt.Println(v)
	}
}

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

	// 1. new一个WaitGroup对象
	var wg = new(sync.WaitGroup)

	// 2. 增加对应的计算值。两个写,增加计数值为2.
	wg.Add(2)
	go send(ch, wg) // 写
	go send(ch, wg) // 写
	go recv(ch)

	// 3. Wait() 阻塞等待所有的写通道协程结束。待计数值变成零,Wait() 才会返回。
	fmt.Println("wait before")
	wg.Wait()
	fmt.Println("wait after")

	// 4. 关闭通道
	close(ch)

	time.Sleep(time.Second)
}

结果:
在这里插入图片描述

8 多路通道

在真实的世界中,还有一种消息传递场景,那就是消费者有多个消费来源,只要有一个来源生产了数据,消费者就可以读这个数据进行消费。这时候可以将多个来源通道的数据汇聚到目标通道,然后统一在目标通道进行消费。

package main

import (
	"fmt"
	"time"
)

// 模拟每隔一会不停的生产一个数
func send(ch chan int, gap time.Duration) {
	i := 0
	for {
		i++
		ch <- i
		time.Sleep(gap)
	}
}

// 收集方法1:将多个原通道内容拷贝到单一的目标通道
func collect(source chan int, target chan int) {
	for v := range source {
		target <- v // ch3 <- ch2 ; ch3 <- ch1
	}
}

// 收集方法2:使用select处理。效果和方法1一样。
func collect2(ch1 chan int, ch2 chan int, target chan int) {
	for {
		select {
		case v := <-ch1:
			target <- v
		case v := <-ch2:
			target <- v
			// default: // 添加default就表示非阻塞
			// 	fmt.Println("collect2")
		}
	}
}

// 从目标通道消费数据
func recv(ch chan int) {
	for v := range ch {
		fmt.Printf("receive %d\n", v)
	}
}

func main() {
	// 1. 创建3个通道,1、2表示多路来源,3表示用来汇总这些来源,这样消费者就可以统一从3读取内容消费。
	var ch1 = make(chan int)
	var ch2 = make(chan int)
	var ch3 = make(chan int)

	// 2. 数据输入到多个通道
	go send(ch1, time.Second)
	go send(ch2, 2*time.Second)

	// 3. 汇总这些数据到统一的通道
	// go collect(ch1, ch3)
	// go collect(ch2, ch3)
	go collect2(ch1, ch2, ch3)

	// 4. 读取进行消费
	recv(ch3)
}

结果:
在这里插入图片描述

9 多路复用select

实际上多路复用select在上面的例子就已经使用到了,关于select详细的说明,可以参考这篇博客GO select用法详解

下面例子是从多个通道直接读取数据,而没有像上面的例子先收集再读取。

// 2-8 多路复用select
package main

import (
	"fmt"
	"time"
)

func send(ch chan int, gap time.Duration) {
	i := 0
	for {
		i++
		ch <- i
		time.Sleep(gap)
	}
}

func recv(ch1 chan int, ch2 chan int) {
	for {
		select {
		case v := <-ch1:
			fmt.Printf("recv %d from ch1\n", v)
		case v := <-ch2:
			fmt.Printf("recv %d from ch2\n", v)
		}
	}
}

func main() {
	var ch1 = make(chan int)
	var ch2 = make(chan int)

	go send(ch1, time.Second)
	go send(ch2, 2*time.Second)

	recv(ch1, ch2)
}

结果:
在这里插入图片描述

10 非阻塞读写

通道的非阻塞读写。当通道空时,读操作不会阻塞,当通道满时,写操作也不会阻塞。非阻塞读写需要依靠 select 语句的 default 分支。当 select 语句所有通道都不可读写时,如果定义了 default 分支,那就会执行 default 分支逻辑,这样就起到了不阻塞的效果。

下面以select写时,添加了default不会阻塞的例子为例。自己可以在case添加读,测试读操作也不会阻塞的例子。

注意这个非阻塞指的是select这个内部IO的default,而不是指通道的读写变成非阻塞,通道本身依然会阻塞,这是不会变的。

package main

import (
	"fmt"
	"time"
)

func send(ch1 chan int, ch2 chan int) {
	i := 0
	for {
		i++
		select {
		case ch1 <- i:
			fmt.Printf("send ch1 %d\n", i)
		case ch2 <- i:
			fmt.Printf("send ch2 %d\n", i)
		default:
			fmt.Printf("ch block\n")
			time.Sleep(2 * time.Second) // 这里只是为了少打印,因为非阻塞会这里打印非常快
		}
	}
}

func recv(ch chan int, gap time.Duration, name string) {
	for v := range ch {
		fmt.Printf("receive %s %d\n", name, v)
		time.Sleep(gap)
	}
}

func main() {
	// 无缓冲通道
	var ch1 = make(chan int)
	var ch2 = make(chan int)

	// 两个消费者的休眠时间不一样,名称不一样
	go recv(ch1, time.Second, "ch1")
	go recv(ch2, 2*time.Second, "ch2")

	send(ch1, ch2)
}

结果:
在这里插入图片描述

11 生产者、消费者模型

实际上上面的例子基本是生产者消费者的模型,这里额外再写一遍,加深印象。

// 2-10 生产者、消费者模型
package main

import (
	"fmt"
	"os"
	"os/signal"
	"syscall"
	"time"
)

// chan   // read-write
// <-chan // read only
// chan<- // write only
// 生产者
func Producer(factor int, out chan<- int) {
	// a := 0
	// a = <-out // 因为只写,所以报错:invalid operation: cannot receive from send-only channel out (variable of type chan<- int)
	// fmt.Println(a)

	for i := 0; ; i++ {
		out <- i * factor
		time.Sleep(3 * time.Second)
	}
}

// 消费者
func Consumer(in <-chan int) {
	//a := 0
	//in <- a // 因为只读,所以报错:invalid operation: cannot send to receive-only type <-chan int
	//a = <-in // ok,因为可以读
	//fmt.Println(a)

	for v := range in {
		fmt.Println(v)
	}
}

func main() {
	ch := make(chan int, 64)

	// 1. 生产者
	go Producer(3, ch) // 生成3的倍数序列
	go Producer(5, ch) // 生成5的倍数序列

	// 2. 消费者
	go Consumer(ch)

	// 信号相关处理。Ctrl +C 退出
	sig := make(chan os.Signal, 1)
	signal.Notify(sig, syscall.SIGINT, syscall.SIGTERM)

	fmt.Printf("wait Ctrl + C\n")
	fmt.Printf("quit (%v)\n", <-sig)
}

结果:
在这里插入图片描述

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值