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阻塞等待读数据:
一般情况下,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语言中文网,里面一些概念讲得比较清楚,可可去看看。