go channel 实现两个协程交替打印

1,394 阅读3分钟

一起养成写作习惯!这是我参与「掘金日新计划 · 4 月更文挑战」的第4天,点击查看活动详情

channel

底层数据结构
 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不允许并发读写
 }

waitqsudog 的一个双向链表

 1. type waitq struct {
 2.    first *sudog
 3.    last  *sudog
 4. }

sudog 实际上是对 goroutine 的一个封装,表示一个在等待队列中的goroutine,该结构

存储了两个分别指向前后sudog的指针用来构成链表

发送数据
  • 如果当前channel的recvq上存在已经被阻塞的Goroutine(也就是说有goroutine在等待读消息),那么会直接将数据发送给当前的Goroutine并将其设置成下一个运行的Goroutine(设置处理器runnext属性,不会立刻调度)
  • 如果channel存在缓冲区并且还有空余位置,会直接将数据存储到缓存区sendx所在的位置上
  • 如果不满足上述两种情况,会创建一个sudog结构并将其加入channel的sendq队列中,当前Goroutine陷入阻塞等待其他协程从Channel接收数据
接收数据
  • 如果Channel为空,那么会直接让出处理器的使用权。

  • 如果Channel已经关闭并且缓存区没有任何数据,会直接返回

  • 如果Channel的sendq队列中存在挂起的Goroutine(说明有阻塞发送的goroutine),根据缓冲区的大小分别处理不同的情况:

    如果 Channel 不存在缓冲区, 将 Channel 发送队列中 Goroutine 存储的数据拷贝到目标内存地址中;

    如果 Channel 存在缓冲区,将队列中的数据拷贝到接收方的内存地址;将发送队列头的数据拷贝到缓冲区中,释放一个阻塞的发送方;

  • 如果Channel的缓冲区存在数据(没有阻塞的发送Goroutine),会将缓冲区中的数据拷贝到接收方的内存地址、清除队列中的数据并完成收尾工作。

  • 当 Channel 的发送队列中不存在等待的 Goroutine 并且缓冲区中也不存在任何数据时,会使用 runtime.sudog 将当前 Goroutine 包装成一个处于等待状态的 Goroutine 将其加入到接收队列中并陷入休眠等待调度器的唤醒;

关闭通道

当 Channel 是一个空指针或者已经被关闭时,Go 语言运行时都会直接崩溃并抛出异常

处理完了这些异常的情况之后就可以开始执行关闭 Channel 的逻辑了,close 函数先上一把大锁,接着把所有挂在这个 channel 上的 sender 和 receiver 全都连成一个 sudog 链表,再解锁。最后,再将所有的 sudog 全都唤醒。

唤醒之后,sender 会检测到channel已经关闭,panic。从一个有缓冲的 channel 里读数据,当 channel 被关闭,依然能读出有效值。只有当返回的 ok 为 false 时,读出的数据才是无效的,为对应类型的零值。

 x, ok := <-ch

实现两个协程交替打印

方法一:使用两个channel

这里channel CA 必须要有缓冲区,否则最后会报错 fatal error: all goroutines are asleep - deadlock!

这是因为无缓冲的通道只有在有接收方能够接收值的时候才能发送成功,否则会一直处于等待发送的阶段。因为最后交替运行完后没有协程可以接收CA通道中的数据,所以会一直阻塞发生死锁

package main

import (
	"fmt"
	"sync"
)


var wg sync.WaitGroup
var CA chan int
var CB chan int

func main(){
	wg = sync.WaitGroup{}
	CA = make(chan int,1)
	CB = make(chan int)
	wg.Add(2)

	go A()
	go B()
	CA<-1
	wg.Wait()

}

func A(){

	for i:=0;i<5;i++{
		<-CA
		fmt.Println(2*i)
		CB<-1
	}
	wg.Done()

}

func B(){

	for i:=0;i<5;i++{
		<-CB
		fmt.Println(2*i+1)
		CA<-1
	}
	wg.Done()

}

方法二 :使用一个channel

使用无缓冲的channel,使两个协程同步,按照相同的步调执行,但因为要交替打印所以不能每次同步后都打印,要让该输出的协程输出,不该输出的协程不输出

package main

import (
   "fmt"
   "sync"
)

var wg sync.WaitGroup
var CA chan int

func main(){
   wg = sync.WaitGroup{}
   CA = make(chan int)

   wg.Add(2)

   go A()
   go B()
   wg.Wait()

}

func A(){

   for i:=0;i<10;i++{
      CA<-1
      if i%2 == 0{
         fmt.Println(i)
      }
   }
   wg.Done()

}

func B(){

   for i:=0;i<10;i++{
      <-CA
      if i%2 == 1{
         fmt.Println(i)
      }

   }
   wg.Done()

}