GO语言学习--goroutine间访问channel阻塞与不阻塞(select中case)

        channel是Golang在语言层面提供的goroutine间的通信方式,channel主要用于进程内各goroutine间通信,了解channel结构,与goroutine访问机制,程序就能很灵活的实现并发编程。

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不允许并发读写

}

从channel读数据,如果channel缓冲区为空或者没有缓冲区,当前goroutine会被阻塞。 向channel写数据,如果channel缓冲区已满或者没有缓冲区,当前goroutine会被阻塞。

被阻塞的goroutine将会挂在channel的等待队列中:

因读阻塞的goroutine会被向channel写入数据的goroutine唤醒;

因写阻塞的goroutine会被从channel读数据的goroutine唤醒;

下图展示了一个没有缓冲区的channel,有几个goroutine阻塞等待读数据:

 

GO语言学习--goroutine间访问channel阻塞与不阻塞(select中case)

一般情况下,recvq和sendq至少有一个为空,上图recvq中取数据的在火急火燎排队等,buf缓存区一有数据就会被拿走,怎么会有sendq排队不去buf缓存区存放数据了,实际上这种情况数据根本不会要存到buf缓存区的,直接从你sendq队列goroutine的数据区就取走了。相似的如果recvq队列为空,sendq队列排队的情况也可同样的机制。

从上面了解goroutine要从channel访问数据,如果遇到为空或满了的情况,该channel就要排队,但也有例外,这情况下,它只是来看一下是否能访问(队列是否为空),如果不能,它下次再来。这个就可以给我们站台排队买票来理解:

package main

import (

"fmt"

"time"

)

func releaseTickets(ticketNumchan int) {

for {

time.Sleep(1 * time.Second)//假设隔一秒钟放一张票

      ticketNum <-1

  }

}

func ticketTout(ticketNumchan int) {

for {

time.Sleep(100 * time.Millisecond)//黄牛党,0.1秒轮询

      fmt.Println("黄牛:我排队咯")

tickets := <- ticketNum//因为放票时距是1秒,部分黄牛也会阻塞

      fmt.Printf("黄牛:又买到%d张票啦\n",tickets)

}

}

func main() {

var ticketBuf = make(chan int,10)

go releaseTickets(ticketBuf)

go ticketTout(ticketBuf)

ticket :=0

  for {

if ticket ==1 {

break //普通购票者买到票了,回家

      }

time.Sleep(300 * time.Millisecond)//普通购票者,0.3秒轮询

      select {

case ticket = <- ticketBuf ://队伍太长了,不想排队,等下再来看看

        fmt.Printf("普通购票者:也买到%d张票啦\n",ticket)//基本不可能

      default:

fmt.Println("普通购票者:唉,不排队就买不到票")

}

}

}

这种情况下,普通购票者不进队列排队,而黄牛不停的进入队列排队,所以买不着票。

case ticket = <- ticketBuf : 

这个case语句执行<- ticketBuf (抢票)是不进入队列,如果不行就返回了。

select的case语句读channel不会阻塞,尽管channel中没有数据。这是由于case语句编译后调用读channel时会明确传入不阻塞的参数,此时读不到数据时不会将当前goroutine加入到等待队列,而是直接返回。

那么如何才能买到票了,又不想把身份证交给黄牛,同时黄牛加价也吓人。有一种办法是叫自己的有时间的朋友(代理人)排队代买啦,然后你就不停打电话问朋友是否买到票,这样就能实现不用在现场排队,又能买到票,落下心来,好安排计划。

package main

import (

"fmt"

"time"

)

func releaseTickets2(ticketNumchan int) {

for {

time.Sleep(1 * time.Second)//假设隔一秒钟放一张票

      ticketNum <-1

  }

}

func ticketTout2(ticketNumchan int) {

for {

time.Sleep(100 * time.Millisecond)//黄牛党,0.1秒轮询

      fmt.Println("黄牛:我排队咯")

tickets := <- ticketNum//因为放票时距是1秒,部分黄牛也会阻塞

      fmt.Printf("黄牛:又买到%d张票啦\n",tickets)

}

}

func proxyMan(ticketNumchan int,ticketchan int) {

//for {

time.Sleep(2 * time.Second)//佛系代理人,2秒轮询,

                                  //但只要排队还是能抢到票的

      ticket <- <-ticketNum//代理人阻塞排队

  //}

}

func main() {

var ticketBuf = make(chan int,10)

var mobAboutTicket = make(chan int,2)

go releaseTickets2(ticketBuf)

go ticketTout2(ticketBuf)

go proxyMan(ticketBuf,mobAboutTicket)//代理人进程

  ticket :=0

  for {

if ticket ==1 {//电话通道里告诉已经买到票了

        fmt.Println("普通购票者:朋友帮我买到票了")

break //普通购票者买到票了,回家

      }

time.Sleep(300 * time.Millisecond)

select {

//case <- ticketBuf : //委托给代理人了,自己就不在现场争通道了

                          //代理人在代理人的进程中随队争,ticketBuf通过参数传给ticketNum

      case ticket = <- mobAboutTicket ://与代理的专用通道,这个不会有其它人来抢了,

                                //非阻塞模式访问都可取到,相当于不停打电话问

                                //因为这个电话两人专用电话,不存在打不进

        fmt.Printf("代理购票者:也买到%d张票啦\n",ticket)

default:

fmt.Println("普通购票者:唉,不排队就买不到票")

}

}

}

以上部分内容来自Go语言中文网,里面一些概念讲得比较清楚,可可去看看。