Go语言(基础)——channel

1、前言

  • channel 是 Go 中很重要的概念,配合 goroutine 是 Go 能够方便实现并发编程的关键。实现并发有两种机制:第一种是消息传递机制,第二种是共享内存机制,Go 中的 channel 则是利用共享内存机制配合 goroutine 实现的并发。
  • 本帖子由浅入深地来介绍 Go 中的 channel,方便以后复习查看。

2、channel 的使用

2.1、声明与创建 channel
2.1.1、双向 channel
  • 所谓双向 channel 就是指既可以往 channel 里写数据,还以从 channel 往外读数据,与之对应的则是单向 channel。
  • 声明 channel
var ch chan dataType
  • dataType 非常广泛,可以是基本数据类型,也可以是 map,slice,自定义的 struct 类型,甚至可以是 channel。所以在 Go 中很容易通过 channel 来共享多种多样的数据。
  • 创建 channel 的时候有两种方式,第一种是无缓冲的 channel,第二种是带缓冲的 channel,创建方式如下:
//方式一:无缓冲
nobufferedch := make(chan int)
//方式二:带缓冲
bufferedch := make(chan int, 5)
2.1.2、单向 channel
  • 所谓单向 channel 就是指只可以往 channel 里写数据,或者只可以从 channel 里往外读数据。
  • 声明 channel
//第一种:send-only channel
var singledirectch chan<- dataType
//第二种:receive-only channel
var singledirectch <-chan dataType
  • 创建 channel 的时候同样有两种方式,第一种是无缓冲的 channel,第二种是带缓冲的 channel,创建方式如下(以 send-only channel 为例):
//方式一:无缓冲
nobufferedch := make(chan<- int)
//方式二:带缓冲
bufferedch := make(chan<- int, 5)
  • 当我们使用 make 去创建一个 channel 的时候,实际上返回的是一个指向 channel 的指针。
2.2、在 channel 上发送和接收数据(入门案例)
  • channel 就像一个消息管道,发送者在管道的 a 端发送消息,接收者就在管道的 b 端接收消息,当消息管道里充满了消息,发送者就不能往里面发送消息了,得停一停,只有当接收者从消息管道取出消息,让管道不再满的时候,之前停下的发送者才能继续往管道里发送消息;同理,当管道里没有消息的时候,接收者也得停一停,等发送者往管道里发送了消息,接收者才能继续接收管道里的消息。
  • 接下来以一个经典的生产者—消费者模型,介绍在 channel 上发送和接收数据。
  • 这里,自行车生产商 Producer 是消息的发送者,购买自行车的客户 Consumer 是消息的接收者,自行车商店 Store 就是消息管道,然后这是一个带缓冲的消息管道,缓冲容量是 5,也就是说,这个 Store 里一次最多只能摆放 5 辆自行车,当 Store 里有 5 辆 自行车的时候,Producer 就要停下来歇一歇了。话不多说,直接上代码。
package main

import (
	"fmt"
	"time"
)

type Bike struct {
	Id uint32
	Brand string
	Location string
}

var Store chan *Bike
var IsFinish chan bool

func init() { //初始化函数,先于 main 函数执行
	Store = make(chan *Bike,5)
	IsFinish = make(chan bool)
}

func Producer() { //自行车生产者
	for i := 0; i < 10; i++ { //总共生产 10 辆自行车
		bike := &Bike{
			Id:       uint32(i),
			Brand:    "凤凰牌",
			Location: "上海",
		}
		Store <- bike
		fmt.Printf("**%s生产者**:%d号%s牌自行车\n", (*bike).Location, (*bike).Id, (*bike).Brand)
		time.Sleep(time.Second * 2) //每隔两秒钟生产一辆自行车
	}
	//生产完之后,要关闭 Store,等到 Store 里的自行车被消费者买完以后,消费者会通过知道 Store 已经关闭了,而不再继续等待买 Store 里的自行车
	close(Store) //close 是非常有必要的!
}

func Consumer() { //自行车消费者
	for {
		bike, ok := <- Store
		if bike == nil && ok == false {
			break
		}
		fmt.Printf("==%s消费者==:%d号%s牌自行车\n", (*bike).Location, (*bike).Id, (*bike).Brand)
		time.Sleep(time.Second * 3) //消费者每隔 3 秒钟买一辆自行车
	}
	IsFinish <- true //告诉主线程 Store 里的自行车已经卖完了,生产者也不生产了,可以结束了
}

func main() {
	go Producer()
	go Consumer()
	<- IsFinish //在 IsFinish <- true 这句代码没执行前,主线程会阻塞在这里,这么做的目的就是防止上面两个协程还没执行完,主线程就提前退出了
}
  • 执行结果如下:
**上海生产者**:0号凤凰牌牌自行车
==上海消费者==:0号凤凰牌牌自行车
**上海生产者**:1号凤凰牌牌自行车
==上海消费者==:1号凤凰牌牌自行车
**上海生产者**:2号凤凰牌牌自行车
==上海消费者==:2号凤凰牌牌自行车
**上海生产者**:3号凤凰牌牌自行车
**上海生产者**:4号凤凰牌牌自行车
==上海消费者==:3号凤凰牌牌自行车
**上海生产者**:5号凤凰牌牌自行车
==上海消费者==:4号凤凰牌牌自行车
**上海生产者**:6号凤凰牌牌自行车
**上海生产者**:7号凤凰牌牌自行车
==上海消费者==:5号凤凰牌牌自行车
**上海生产者**:8号凤凰牌牌自行车
==上海消费者==:6号凤凰牌牌自行车
**上海生产者**:9号凤凰牌牌自行车
==上海消费者==:7号凤凰牌牌自行车
==上海消费者==:8号凤凰牌牌自行车
==上海消费者==:9号凤凰牌牌自行车
2.3、channel 与 select 的结合使用
2.3.1、入门案例
  • select 用于多个 channel 监听并收发消息,当任何一个 case 满足条件则会执行,若没有可执行的 case,就会执行 default,如果没有 default,程序就会阻塞。
func SelectSample() {
	c1 := make(chan string)
	go func(c chan string) {
		time.Sleep(time.Duration(rand.Intn(10)) * time.Second)
		c <- "协程一来也!"
	}(c1)
	c2 := make(chan string)
	go func(c chan string) {
		time.Sleep(time.Duration(rand.Intn(10)) * time.Second)
		c <- "协程二来也!"
	}(c2)

	for {
		select {
		case v, ok := <-c1:
			fmt.Println(v, ok)
		case v, ok := <- c2:
			fmt.Println(v, ok)
		default:
			fmt.Println("waiting")
		}
		time.Sleep(time.Second)
	}
}
  • 在 main 函数里调用 SelectSample 函数,执行结果如下:
waiting
协程一来也! true
waiting
waiting
waiting
waiting
waiting
协程二来也! true
waiting
waiting
waiting
......
2.3.2、超时处理
  • 许多情况下,我们使用 select 监听 channel,然后根据 channel 的状态作出下一步指示,并且我们经常使用 default 来配合操作,default 的用处就是在所有 case 都不匹配的情况下,执行 default 后面的语句,但是存在一种情况,就是虽然目前监听的 channel 状态没有发生变化(channel 没有写入数据或读出数据),但是可能经过一段时间后,channel 状态就会发生变化,所以这个时候,我们需要给程序一点等待的时间,而不是立马去执行 default 后面的语句。说到这里可能有的人会认为,既然是等待一段时间,那我用 time.Sleep(time.Duration) 不就好了,这么想确实没错,但是有一个问题,你要让程序睡眠多久合适呢,你不确定让程序睡眠(等待)多久。那么现在我就来介绍另一个方法:time.After(time.Duration),这个函数执行返回一个 channel,此处不妨让 timeout := time.After(time.Duration),这个函数的作用就是在 time.Duration 时间后,执行并返回 timeout,它是一个 channel,既然是 channel,那么我们就可以把它放到 case 条件后,和其他 channel 一起“竞争”呀。话不多说,上代码:
func Timeout1() {
	c := make(chan string, 1)
	finish := make(chan bool)
	go func() {
		time.Sleep(time.Second * 3) //故意等待 3 秒
		c <- "message"
	}()

	go func() {
		for {
			select {
			case res := <-c:
				fmt.Println(res, time.Now())
				finish <- true
				break
			case val, ok := <-time.After(time.Second * 2): //超时时间设置为 2 秒,2 秒后返回 timeout
				fmt.Println("timeout", val, ok)
			}
		}
	}()
	<- finish
}
  • 将上面代码放入 main 中执行,得到的输出结果如下,通过输出结果发现,两条语句前后输出时间间隔为 1 秒
timeout 2021-03-26 20:55:44.47262 +0800 CST m=+2.007340901
message 2021-03-26 20:55:45.4725173 +0800 CST m=+3.007238201

3、需要注意的点

3.1、使用 for-range 迭代输出 channel
  • 当使用 for-range 迭代输出 channel 里的数据的时候,必须在某个位置明确的关闭这个 channel,不然会遇到死锁情况,用一段代码演示一下:
func DeadLockSample() {
	c := make(chan int)
	go func(c chan int) {
		fmt.Println("in goroutine")
		c <- 100
		//close(c)
	}(c)
	for {
		val, ok := <-c
		fmt.Println(val, ok)
	}
}
  • 上面代码在 main 函数中执行的输出结果如下:
in goroutine
100 true
fatal error: all goroutines are asleep - deadlock!

goroutine 1 [chan receive]:
goStudy/self/Channel.DeadLockSample()
	D:/program/Code/go/src/goStudy/self/Channel/test_channel.go:125 +0x7b
main.main()
	D:/program/Code/go/src/goStudy/self/main.go:56 +0x27

Process finished with exit code 2
  • 那如果把上面 close© 前面的注释符号取消呢,执行结果会怎样的呢?请看下面的执行结果:
in goroutine
100 true
0 false
0 false
0 false
0 false
......
3.2、time.After() 的注意事项
  • time.After() 函数会在每次 select 的时候,重新计时,更通俗地说,也就是 time.After() 在每次被执行的时候都会重新开始计时,比如我设置的超时时间是 3 秒,有一个任务需要 2 秒才能执行完成一次,并且执行完后往一个 channel(work) 中写数据,然后这个任务会循环执行 5 次,现在 time.After 与 work 都在 select 的 case 条件后面等待。我们假设程序在第 0 秒的时候开始执行,那么在 第 2 秒的时候,任务顺利执行一次,第 4 秒的时候,任务又顺利执行一次,直到第 10 秒的时候,任务执行完了,超时任务都没被触发执行一次,不解释了,直接上代码:
func Timeout4() {
	work := make(chan string)
	finish := make(chan bool)
	go func() {
		for i := 0; i < 5; i++ {
			time.Sleep(time.Second * 1)
			work <- "任务" + strconv.Itoa(i)
		}
	}()
	go func() {
		for {
			select {
			case val := <- work:
				fmt.Println(val)
				if strings.HasSuffix(val, "4") {
					finish <- true
				}
			case <-time.After(time.Second * 3):
				fmt.Println("超时了...")
			}
		}
	}()
	<- finish
}
  • 将以上代码放入 main 中执行,执行结果如下:
任务0
任务1
任务2
任务3
任务4
  • 解释:原因就是任务是两秒钟执行一次,而超时时间设置的是三秒,现在程序开始执行,过了两秒,任务被执行一次,然后 work 嘲笑 time.After:“哈哈哈,我获得执行机会咯,而你又要重头开始等待咯”。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Wuxy_Fansj_Forever

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值