Golang中的channel

单纯地将函数并发没有意义,函数与函数之间需要数据交换。 

共享内存可以进行数据交换,但是在不同的协程里面容易发生竞态问题,为了保证数据交换的正确性,很多数据交换的正确性,很多并发模型中必须使用互斥量对内存需要加锁,这样会造成性能下降。

Golang提倡通信共享内存而不是通过共享内存而实现通信。

通道有两个操作,接收和发送,发送是发送值到通道,接收是从通道里面接收值。

通道panic的三种情况 :关闭已经关闭的通道;对关闭过的通道再发送值;关闭一个值为nil的通道。

无缓冲通道只有在接收方能够能够接收值的时候才能发送,否则一直处于等待发送的阶段。

func main() {
	ch := make(chan int)
	ch <- 10
	fmt.Println("发送成功")
}

 上面的代码就声明了一个无缓冲的通道,因为没有接收方来接收值,所以通道一直处于等待发送的状态,从而导致了死锁。

我们可以使用创建另外一个goroutine去接收值

func recv(c chan int) {
	ret := <-c
	fmt.Println("接收成功", ret)
}

func main() {
	ch := make(chan int)
	go recv(ch) // 创建一个 goroutine 从通道接收值
	ch <- 10
	fmt.Println("发送成功")
}

 如果如果是 ch <- 10 这行代码先执行的话,无缓冲通道ch上的发送操作会阻塞,直到另外一个goroutine在该通道上执行接收操作,这个时候数字10才能发送成功

同理,无缓冲通道只有在发送方能够能够发送值的时候才能接收,否则一直处于等待接收的阶段。

以上两种情况都会可能会导致死锁。

通道的底层数据结构

type hchan struct {
  qcount uint // 当前队列中剩余元素个数
  dataqsiz uint // 环形队列长度,即可以存放的元素个数
  buf unsafe.Pointer // 环形队列指针
  elemsize uint16 // 每个元素的大小
  closed uint32 // 标识关闭状态
  elemtype *_type // 元素类型
  sendx uint // 队列下标,指示元素写入时存放到队列中的位置
  recvx uint // 队列下标,指示元素从队列的该位置读出
  recvq waitq // 等待读消息的goroutine队列
  sendq waitq // 等待写消息的goroutine队列
  lock mutex // 互斥锁,chan不允许并发读写
}

channel 主要是由buf环形队列指针、goroutine等待队列、互斥锁组成等

环形队列指针作为缓冲区

等待队列:被阻塞的goroutine 将会挂在channel的等待队列(等待写消息队列和等待读消息队列)中

向channel中读数据,如果channel缓冲区为空或者没有缓冲区,当前goroutine会被阻塞,被阻塞的goroutine会被挂在channel中的recvq 等待队列中。 在recvq中的gorouine会被向channel中写入数据的goroutine唤醒。

向channel中写数据,如果channel缓冲区已满或者没有缓冲区,当前goroutine会被阻塞,被阻塞的goroutine会被挂在channel中的sendq 等待队列中。 在sendq中的gorouine会被从channel中读取数据的goroutine唤醒。

 一般情况下,receq和sendq至少有一个为空。

互斥锁:主要是为了线程安全,在对数据进行入队列和出队列的时候,需要先获取互斥锁。

向channel中写入数据的过程

  1. 如果等待接收队列recvq不为空,说明有很多协程从通道中接收数据都没有成功,被阻塞的,说明缓冲区里面没有数据或者没有缓冲区,这个时候直接从recvq中取出来G,并把数据直接写入通道即可,然后把该G唤醒,结束发送过程。(缓冲区有空余位置)
  2. 如果等待接收队列recvq为空,说明缓冲区中有数据,我们需要查看一下缓冲区中是否有空余位置,如果有的话,将数据写入缓冲区,然后结束发送过程即可 (检查是否有空余位置)
  3. 如果缓冲区没有空闲位置,那将待发送数据写入G,将当前G加入sendq,进入睡眠,等待被读goroutine协程唤醒。 (缓冲区没有空余位置)

从channel中读取数据的过程

  1. 如果等待发送队列sendq不为空,说明有很多协程向通道中发送数据都没有成功,被阻塞的,说明缓冲区中数据满了或者没有缓冲区 
  2. 如果没有缓冲区,则直接从sendq中取出来G,把G中的数据读取,然后把G唤醒,结束读取进程  (没有缓冲区,需要查看sendq是否为空,不为空也可以直接读取)
  3. 如果是缓冲区满了,这个时候从sendq中取出来G,并把缓冲区的首部数据直接取出,然后把G中数据写入缓冲区尾部,把G唤醒,结束读取过程 (缓冲区有数据,直接从缓冲区中读取即可,读取走了一个,需要把sendq里面的G写入的放在缓冲区末尾)
  4. 如果等待发送队列sendq为空的话,说明缓冲区中没有满,我们查看一下缓冲区中是否有数据,如果有的话,直接从缓冲区里面读取数据即可,然后结束读取过程 (缓冲区有数据,直接取出来即可)
  5. 如果缓冲区没有数据的话,那直接将读取数据的协程写入G,将当前G加入receq中,进入睡眠,等待被写goroutine协程唤醒 (缓冲区没有数据,需要阻塞)
     

Channel中Panic的情况

  • 关闭值为nil的channel
  • 关闭已经关闭的channel
  • 向已经关闭的channel写数据

Channel死锁的场景:

向一个nil的channel发送、接收数据

无缓冲channel只读不写,或者是只写不读

无缓冲通道在一个协程中,读操作在写操作的前面

多个协程互相等待
 

Channel的应用场景:

定时任务、解耦生产者消费者、控制并发数量、顺序

如何控制协程并发数量?

在 Golang 中,可以使用 sync.WaitGroup 和有缓冲通道(buffered channel)来控制协程的并发数量。

sync.WaitGroup 是一个计数信号量,用于等待一组协程完成执行。通过 Add() 方法增加计数,通过 Done() 方法减少计数,通过 Wait() 方法阻塞主goroutine,直到计数器归零。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值