golang channel总结(很强-很全)

go channel 学习

channel特性

1、channel,可译为通道,是go语言协程goroutine之间的通信方式。
2、channel通信可以想象成从管道的一头塞进数据,从另一头读取数据。
3、协程通过channel通信可以不用进行加锁操作。
4、把数据发往无缓冲通道,如果接收方没有接收。发送操作将持续阻塞,此时会 释放cpu,执行其他协程,并且查看其他携程是否能够解除阻塞
5、接收将持续阻塞直到发送方发送数据

channel的创建和使用

c1 := make(chan int) // 无缓冲
c2 := make(chan interface{}) // 任意类型通道
c3 := make(chan int, 1) // 有缓冲

type Str struct{}
c4 := make(chan *Str) // 指针类型通道
c5 := make(chan struct{})
// 从channel变量c中读取数据,保存到变量v中
v := <-c
// 从channel变量c中读取数据,数据直接丢弃
<-c

接收通道数据(阻塞和非阻塞-select)

1、阻塞接收数据

data:= <- ch

2、非阻塞接收数据(select)

data, ok := <-ch
为了能知道当前 channel 是否被关闭,可以使用下面的写法来判断。
if v, ok := <-ch; !ok {
	fmt.Println("channel 已关闭,读取不到数据")
}
还可以使用下面的写法不断的获取 channel 里的数据:
for data := range ch {
	// get data dosomething
}
非阻塞可能会造成高cpu, 使用较少
实现接收超时检测, 可以使用 select 和 计时器channel 

select {
	case ack:= <- ch:
		return ack, nil
	case <- time.After(time.Second):
		return "", errors.New("time out")
}

在写程序时,有时并不单单只会和一个 goroutine 通信,当我们要进行多 goroutine 通信时,则会使用 select 写法来管理多个 channel 的通信数据:

func Select() {
	ch1 := make(chan struct{})
	ch2 := make(chan struct{})
	// ch1, ch2 发送数据
	go func (){
		for{
			fmt.Println("send ch1")
			ch1<- struct{}{}
			ch2<- struct{}{}
			time.Sleep(10*time.Second)
		}
	}()
	go func (){
		for {
			fmt.Println("send ch2")
			ch2<- struct{}{}
			ch1<- struct{}{}
			time.Sleep(10*time.Second)
		}
	}()
	//延时等待数据先发送,在接收,模拟数据同时到达select的处理
	time.Sleep(4*time.Second)
	// channel 数据接受处理
	for {
		select {
			case <-ch1:
				fmt.Println("read- ch1")
			case <-ch2:
				fmt.Println("read ch2")
		//default:
		//	fmt.Println("test default")
		}
		fmt.Println("continue -------")
	}
}
------------------------------------------------------
执行结果如下:
send ch2
send ch1
read ch2
continue -------
read- ch1
continue -------
read ch2
continue -------
read- ch1
continue -------

注意:

  1. 图中select中的default 会导致select无阻塞,也会导致cpu飙高问题
  2. 去掉default,select会阻塞,直到通道有数据时解除
  3. 当两个通道同时有数据产生时,选择其中一个通道去执行,直到所有通道数据都处理完毕–

无缓冲channel

1、如果在一个协程里写这样的代码,一定会死锁:

func main(){
	ch := make(chan int)
	ch <- 1
	<- ch
}

无缓冲的channel的读写者必须同时完成发送和接收,而不能串行,显然单协程无法满足。所以这里造成了循环等待,会死锁。

channel的deadlock

往 channel 里读写数据时是有可能被阻塞住的,一旦被阻塞,则需要其他的 goroutine 执行对应的读写操作,才能解除阻塞状态。

然而,阻塞后一直没能发生调度行为,没有可用的 goroutine 可执行,则会一直卡在这个地方,程序就失去执行意义了。此时 Go 就会报 deadlock 错误,如下代码:
读channel和写channel都需要出现,单独出现会死锁

func main(){
	ch := make(chan int)
	ch <- 1
}
func main(){
	ch := make(chan int)
	<-ch
}

带缓冲channel

阻塞条件:

  1. 通道被填满时,尝试再次发送数据发生阻塞
  2. 通道中没数据时,尝试接收数据时会发生阻塞

解释无缓冲channel和有缓冲channel的例子

  1. 无缓冲通道保证收发过程同步,类似于快递员给你打电话让你下楼取快递,递交快递的过程是同步的,你下楼取快递,过程结束
  2. 有缓冲通道类似于快递柜,快递员将快递放到快递柜中,通知你来取快递,快递员和用户成异步收发过程

关闭channel(有缓冲通道)

close(ch) //关闭通道
cap(ch) //计算通道的容量
  1. 给被关闭的通道发送数据将会触发panic
  2. 从已关闭的通道中接收数据时将不会发生阻塞
func main() {
	ch := make(chan int, 2)
	ch <- 0
	ch <- 1
	close(ch)
	for i:=0;i<cap(ch)+1;i++ {
		v, ok := <- ch
		fmt.Println(v, ok)
	}
}
-----------------
0 true
1 true
0 false

channel使用空结构体

特点:省内存,尤其在事件通信的时候,空结构体不占内存

channel := make(chan struct{})
go func() {
    // ... do something
    channel <- struct{}{}
}()
fmt.Println(<-channel)

channel作为形参的练习

// <-chan  出通道
func Comsume(channel <-chan int) {
	for {
		data := <-channel
		if data == 0 {
			break
		}
		fmt.Println(data)
		time.Sleep(1 * time.Second)
	}
}
// chan<- 进入通道
func Producer(channel chan<- int) {
	for i := 1; i < 10; i++ {
		fmt.Println("wait")
		channel <- i
		fmt.Println("start")
	}
	channel <- 0
}
func ChanTets() {
	channel := make(chan int)
	go Comsume(channel)
	Producer(channel)
}

多生产、单消费模型

func ProducerTest(header string, channel chan<- string) {
	for {
		channel <- fmt.Sprintf("%s: %v", header, rand.Int31())
		time.Sleep(1*time.Second)
	}
}
func CustomerTest(channel <-chan string) {
	for {
		message := <-channel
		fmt.Println(message)
	}
}
func Pcmodel() {
	channel := make(chan string)
	go ProducerTest("cat", channel)
	go ProducerTest("dog", channel)
	CustomerTest(channel)
}

channel 的底层原理

channel 创建后返回了 hchan 结构体,现在我们来研究下这个结构体,它的主要字段如下:

type hchan struct {
	qcount   uint   // channel 里的元素计数
	dataqsiz uint   // 可以缓冲的数量,如 ch := make(chan int, 10)。 此处的 10 即 dataqsiz
	elemsize uint16 // 要发送或接收的数据类型大小
	buf      unsafe.Pointer // 当 channel 设置了缓冲数量时,该 buf 指向一个存储缓冲数据的区域,该区域是一个循环队列的数据结构
	closed   uint32 // 关闭状态
	sendx    uint  // 当 channel 设置了缓冲数量时,数据区域即循环队列此时已发送数据的索引位置
	recvx    uint  // 当 channel 设置了缓冲数量时,数据区域即循环队列此时已接收数据的索引位置
	recvq    waitq // 想读取数据但又被阻塞住的 goroutine 队列
	sendq    waitq // 想发送数据但又被阻塞住的 goroutine 队列

	lock mutex //同步锁-互斥锁
	...
}
recvq和sendq分别是接收(<-channel)或者发送(channel <- xxx)的goroutine抽象出来的结构体(sudog)的队列。是个双向链表
无缓冲 channel读写原理
channel 先写再读

在这里,我们暂时认为有 2 个 goroutine 在使用 channel 通信,按先写再读的顺序,则具体流程如下:
在这里插入图片描述
可以看到,由于 channel 是无缓冲的,所以 G1 暂时被挂在 sendq 队列里,然后 G1 调用了 gopark 休眠了起来。

接着,又有 goroutine 来 channel 读取数据了:

在这里插入图片描述
此时 G2 发现 sendq 等待队列里有 goroutine 存在,于是直接从 G1 copy 数据过来,并且会对 G1 设置 goready 函数,这样下次调度发生时, G1 就可以继续运行,并且会从等待队列里移除掉。

注意:缓存链表中以上每一步的操作,都是需要加锁操作的!

  • 每一加粗样式步的操作的细节可以细化为:
  • 第一,加锁
  • 第二,把数据从goroutine中copy到“队列”中(或者从队列中copy到goroutine中)
  • 第三,释放锁
channel 先读再写

G1 暂时被挂在了 recvq 队列,然后休眠起来。
在这里插入图片描述
G2 在写数据时,发现 recvq 队列有 goroutine 存在,于是直接将数据发送给 G1。同时设置 G1 goready 函数,等待下次调度运行。
在这里插入图片描述

有缓冲 channel 读写原理
channel 先写再读

这一次会优先判断缓冲数据区域是否已满,如果未满,则将数据保存在缓冲数据区域,即环形队列里。如果已满,则和之前的流程是一样的。
在这里插入图片描述
当 G2 要读取数据时,会优先从缓冲数据区域去读取,并且在读取完后,会检查 sendq 队列,如果 goroutine 有等待队列,则会将它上面的 data 补充到缓冲数据区域,并且也对其设置 goready 函数。
在这里插入图片描述

channel 先读后写

此种情况和无缓冲的先读再写是一样流程,此处不再重复说明。

写数据注意:
  • 向一个nil channel发送数据,会调用gopark函数将当前goroutine挂起
  • 向一个已经关闭的channel发送数据,直接会panic
  • 如果channel的recvq当前队列中有被阻塞的接收者,则直接将数据发送给当前goroutine
  • 当channel的缓冲区还有空闲空间,则将数据发送到sendx指向缓冲区的位置
  • 当没有缓冲区或者缓冲区满了,则会创建一个sudog的结构体将其放到channel的sendq队列当中陷入休眠等待被唤醒
读数据注意:
  • 从一个nil channel接收数据,会调用gopark函数将当前goroutine挂起,让出处理器的使用权
  • 从一个已经关闭并且缓冲区中没有元素的channel中接收数据,则会接收到该类型的默认元素,并且第二个返回值返回false
  • 如果channel没有缓冲区且sendq的队列有阻塞的goroutine,则把sendq队列头的sudog中保存的元素值copy到目标地址中
  • 如果channel有缓冲区且缓冲区里面有元素,则把recvx指向缓冲区的元素值copy到目标地址当中,sendq队列头的sudog的元素值copy到recvx指向缓冲区位置的地址当中
  • 当上面的条件都不符合时,则会创建一个sudog的结构体将其放到channel的recvq队列当中陷入休眠等待被唤醒
  • 1
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值