go并发之路(三)——根据无缓冲队列实现一个乒乓球游戏(失败版)

633 阅读3分钟

本文已参与「掘力星计划」,赢取创作大礼包,挑战创作激励金。

需求分析

乒乓球可以用一个无缓冲的队列模拟,打球过程也很符合无缓冲队列的特点,只有一个人击球,另一个人才会击球。

对于接球成功的判断,可以采用随机数随机模拟,不过单纯的模拟不足以感受到无缓冲队列的乐趣,我们不防设计一个超时时间,超时后会接收到另一个通道的数据,如果先接到了第二个通道的数据,我们就理解为没有接到球。

理清了接球判断是否成功的方法以后,我们就可以得到接球发球的过程。

发球:选手向队列写入,不过考虑超时因此可以设置经过随机数秒后发球,表示这一球的快慢程度,随机数越大球越快,这一球越不好接。

接球:选手加载一个超时通道,代表反应速度,如果先加载了超时通道,代表没接到球,反之则代表接到了球。

具体实现

主函数如下


var wg sync.WaitGroup

func main(){
   runtime.GOMAXPROCS(1)
   // 创建一个无缓冲的通道
   court := make(chan int)
   // 计数加 2,表示要等待两个 goroutine
   wg.Add(2)
   // 启动两个选手
   go player("Nadal", court)
   go player("Djokovic", court)
   // 发球
   court <- 1
   // 等待游戏结束
   wg.Wait()
}

player函数看起来很好的模拟了发球和接球过程以及对于超时未接到球的处理

// 模拟一个选手打球
func player(name string, court chan int){
   // 在函数退出时调用Done来通知main函数工作已经完成
   defer wg.Done()
   ball := 0
   var ok bool
   for {
      // 接球
      timeout := make(chan int)
      go func() {
         time.Sleep(1 * time.Second)
         timeout <- 1
         close(timeout)
      }()

      select {
       case ball ,ok = <- court:
          if !ok {
             // 对方没发球,说明失败
             fmt.Printf("Player %s Won\n", name)
             return
          }
       case  <- timeout:
         //接球失败
          fmt.Printf("Player %s Missed\n", name)
          close(court)
          return
      }
      //发球
      fmt.Printf("Player %s Hit %d\n", name, ball)
      c := rand.Intn(1300)
      time.Sleep(time.Duration(c) * time.Millisecond)
      ball++
      court <- ball
   }
}

然而我们发现这个运行结果不尽人意。

Player Djokovic Hit 1
Player Nadal Hit 2
Player Djokovic Missed
panic: send on closed channel

bug分析

查看发现是我们超时后会关闭通道court,但此时我们另一个协程很可能会继续向court写入ball,造成panic。

问题的关键在于,我们以为time.sleep函数是协程一起空等1300毫秒后,程序继续运行,实际上,协程A等待时,协程B已经判断完超时了,这个时候通道就会关闭。

所有如果想要有所改变的话最简单的办法是降低发球时的时延,使其小于1000ms,但这样就会进入新的问题——不断循环两人击球,因为超时永远都不会被触发。

所以目前我们发现

  • 错误的超时机制的设计让我们没办法正确运行
  • 为了做出改变,我们希望最好有确定court通道是否关闭的办法

总结

后来我找到了解决问题的办法,在之后的文章中会提及,也看到了很多优秀的博客,这或许是更有趣的学习方式。