go中的chan

chan数据结构

前言

channel是Golang提供goroutine的通信方式,channel主要用于进程内各goroutine间通信。:
src/runtime/chan.go:hchan 定义了channel的数据结构:
在这里插入图片描述
qcount:当前队列中的剩余的元素个数
dataqsiz:当前队列的长度,即可以存放的元素个数
buf:用来保存goroutine之间传递数据的循环链表
elemsize:每个元素的大小
closed:标识关闭状态
elementtype:元素类型
sendx:元素写入时存放队列中的位置
recvx:队列读出元素的位置
recvq:等待读消息的goroutine队列
sendq:等待写消息的goroutine队列
lock:保证channel写入和读取数据时线程安全的互斥锁
在这里插入图片描述
总结hchan结构体的主要组成部分有四个:

用来保存goroutine之间传递数据的循环链表。=> buf。
用来记录此循环链表当前发送或接收数据的下标值。
=> sendx和recvx。
用于保存向该chan发送和从改chan接收数据的goroutine的队列。=====> sendq 和 recvq
保证channel写入和读取数据时线程安全的锁。 =====> lock

举个栗子

/G1
func sendTask(taskList []Task) {
	...
  
	ch:=make(chan Task, 4) // 初始化长度为4的channel
	for _,task:=range taskList {
		ch <- task  //发送任务到channel
	}
  
	...
}

//G2
func handleTask(ch chan Task) {
	for {
		task:= <-ch //接收任务
		process(task) //处理任务
	}
}

ch是长度为4的带缓冲的channel,G1是发送者,G2是接收者

初始hchan结构体重的buf为空,sendx和recvx均为0。
当G1向ch里发送数据时,首先会对buf加锁,然后将数据copy到buf中,然后sendx++,然后释放对buf的锁。
当G2消费ch的时候,会首先对buf加锁,然后将buf中的数据copy到task变量对应的内存里,然后recvx++,并释放锁。

可以发现整个过程,G1和G2没有共享的内存,底层是通过hchan结构体的buf,并使用copy内存的方式进行通信,最后达到了共享内存的目的,这里也体现了Go中的CSP并发模型。

底层实现:环形队列

chan内部实现了一个环形队列作为其缓冲区,队列的长度是创建chan时指定的。
下图展示了一个可缓存6个元素的channel示意图:
在这里插入图片描述

等待队列

从channel读数据,如果channel缓冲区为空或者没有缓冲区,当前goroutine会被阻塞。向channel写数据,如果channel缓冲区已满或者没有缓冲区,当前goroutine会被阻塞。
被阻塞的goroutine将会挂在channel的等待队列中:

  1. 因读阻塞的goroutine会被向channel写入数据的goroutine唤醒;
  2. 因写阻塞的goroutine会被从channel读数据的goroutine唤醒;
    注意,一般情况下recvq和sendq至少有一个为空。只有一个例外,那就是同一个goroutine使用select语句向channel一边写数据,一边读数据
    一个channel只能传递一个类型的值,仅允许被同一个goroutine读写(锁)

创建channel

创建channel的过程实际上就是对hchan初始化的过程,类型信息和缓冲区长度由remake传入,buf的大小则与元素大小和缓冲区长度共同决定

向channel写数据

向一个channel中写数据简单过程如下:

  1. 如果等待接收队列recvq不为空,说明缓冲区中没有数据或者没有缓冲区,此时直接从recvq取出G,并把数据写入,最后把该G唤醒,结束发送过程;(G->goroutine)
  2. 如果缓冲区中有空余位置,将数据写入缓冲区,结束发送过程;
  3. 如果缓冲区中没有空余位置,将待发送数据写入G,将当前G加入sendq(接收队列),进入睡眠,等待被读goroutine唤醒;
    在这里插入图片描述

从channel读数据

从一个channel读数据简单过程如下:

  1. 如果等待发送队列sendq不为空,且没有缓冲区,直接从sendq中取出G,把G中数据读出,最后把G唤醒,结束读取过程;
  2. 如果等待发送队列sendq不为空,此时说明缓冲区已满,从缓冲区中首部读出数据,把G中数据写入缓冲区尾部,把G唤醒,结束读取过程;
  3. 如果缓冲区中有数据,则从缓冲区取出数据,结束读取过程;
  4. 将当前goroutine加入recvq,进入睡眠,等待被写goroutine唤醒;

关闭channel

关闭channel时会把recvq中的G全部唤醒,本该写入G的数据位置为nil。把sendq中的G全部唤醒,但这些G会panic
除此之外,panic出现的常见场景还有:

  1. 关闭值为nil的channel
  2. 关闭已经被关闭的channel
  3. 向已经关闭的channel写数据

单项channel

实际上就是通过参数传递来控制的
使用select可以监控多channel,比如监控多个channel,当其中某一个channel有数据时,就从其读出数据。
一个简单的示例程序如下:

package main

import (
	"fmt"
	"time"
)

func addChannel(chanName chan int){
	for {
		chanName <- 1//写入数据
		time.Sleep(1 * time.Second)
	}
}

func main() {
	var chan1 = make(chan int,10) //channel的元素类型为Int,channel的容量大小为10
	var chan2 = make(chan int ,10)
	go addChannel(chan1)
	go addChannel(chan2)
	for {
		select {
		case e := <- chan1://读数据
			fmt.Printf("Get element from chan1: %d\n", e)
		case e := <- chan2:
			fmt.Printf("Get element from chan2: %d\n", e)
		default:
			fmt.Printf("No element in chan1 and chan2.\n")
			time.Sleep(1 * time.Second)

		}
	}
}

输出结果

No element in chan1 and chan2.
Get element from chan2: 1
Get element from chan1: 1
Get element from chan1: 1
Get element from chan2: 1
No element in chan1 and chan2.
No element in chan1 and chan2.
Get element from chan1: 1
Get element from chan1: 1
Get element from chan2: 1
Get element from chan2: 1
No element in chan1 and chan2.
No element in chan1 and chan2.
Get element from chan1: 1
Get element from chan2: 1
No element in chan1 and chan2.
Get element from chan2: 1
Get element from chan2: 1
Get element from chan1: 1
Get element from chan1: 1
No element in chan1 and chan2.
Get element from chan1: 1
No element in chan1 and chan2.
Get element from chan2: 1
....

从输出可见,从channel中读出数据的顺序是随机的,事实上select语句的多个case执行顺序是随机的
通过这个示例想说的是:select的case语句读channel不会阻塞,尽管channel中没有数据。这是由于case语句编译后调用读channel时会明确传入不阻塞的参数,此时读不到数据时不会将当前goroutine加入到等待队列,而是直接返回

range

通过range可以持续从channel中读出数据,好像在遍历一个数组一样,当channel中没有数据时会阻塞当前goroutine,与读channel时阻塞处理机制一样。
注意:如果向此channel写数据的goroutine退出时,系统检测到这种情况后会panic,否则range将会永久阻塞

package main

import (
	"fmt"
	"time"
)

func addChannel(chanName chan int){
	for {
		chanName <- 1//写入数据
		time.Sleep(1 * time.Second)
	}
}

func chanRange (chanName chan int){
	for e := range chanName{
		fmt.Printf("Get element from chan: %d\n", e)
	}
}

func main() {
	var chan1 = make(chan int,10) //channel的元素类型为Int,channel的容量大小为10
	//var chan2 = make(chan int ,10)
	go addChannel(chan1)
	chanRange(chan1)
	//go addChannel(chan2)
	//for {
	//	select {
	//	case e := <- chan1://读数据
	//		fmt.Printf("Get element from chan1: %d\n", e)
	//	case e := <- chan2:
	//		fmt.Printf("Get element from chan2: %d\n", e)
	//	default:
	//		fmt.Printf("No element in chan1 and chan2.\n")
	//		time.Sleep(1 * time.Second)
	//
	//	}
	//}
}

输出

Get element from chan: 1
Get element from chan: 1
Get element from chan: 1
Get element from chan: 1
....

推荐文章:https://juejin.cn/post/7037656471210819614

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
引用\[1\]提到,GO语言关键字"chan"用于定义通道(channel)。通道是一种用于在协程(goroutine)之间传递数据的机制,类似于消息队列。在GO语言,可以使用关键字"chan"来声明通道变量,例如"var ch chan string"。通道可以是无缓冲的(阻塞型通道)或有缓冲的(缓冲型通道)。无缓冲的通道在接收数据前会阻塞发送方,直到有接收方准备好接收数据。有缓冲的通道可以在缓冲区未满时发送数据,而不会阻塞发送方。\[1\] 在引用\[2\]的示例代码,可以看到使用了关键字"chan"来定义了两个通道变量c1和c2。这些通道用于在并行协程传递数据。通过使用关键字"select",可以选择等待多个通道操作,从而实现并发的控制流程。在示例代码,使用了"select"来等待c1和c2通道的值,并根据接收到的值进行相应的处理。\[2\] 综上所述,关键字"chan"在GO语言用于定义通道,通道是一种用于在协程之间传递数据的机制,可以通过关键字"select"来选择等待多个通道操作。 #### 引用[.reference_title] - *1* *2* *3* [go语言的25个关键字(基础)](https://blog.csdn.net/Zoffan/article/details/123028545)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v91^control_2,239^v3^insert_chatgpt"}} ] [.reference_item] [ .reference_list ]

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值