channel是GO语言并发体系中的主推的通信机制,它可以让一个 goroutine 通过它给另一个 goroutine 发送值信息。每个 channel 都有一个特殊的类型,也就是 channels 可发送数据的类型。一个可以发送 int 类型数据的 channel 一般写为 chan int。Go语言提倡使用通信的方法代替共享内存,当一个资源需要在 goroutine 之间共享时,通道在 goroutine 之间架起了一个管道,并提供了确保同步交换数据的机制。声明通道时,需要指定将要被共享的数据的类型。可以通过通道共享内置类型、命名类型、结构类型和引用类型的值或者指针。
channel是一种特殊的类型是保证协程安全的,也就是在任何时候,同时只能有一个 goroutine 访问通道进行发送和获取数据。而且遵循先入先出(First In First Out)的规则,保证收发数据的顺序。这两个特性是channel可以产生共享内存功能的重要原因。。读完这一讲,下面我们就可以继续我们的例子,开始GO语言并发的实战了
一、通道的声明
1.经典方式声明
通过使用chan类型,其声明方式如下:
var name chan type
其中type表示通道内的数据类型;name:通道的变量名称,不过这样创建的通道只是空值 nil,一般来说都是通道都是通过make函数创建的。
2.make方式
make函数可以创建通道格式如下:
name := make(chan type)
3.创建带有缓冲的通道
后面会讲到缓冲通道的概念,这里先说他的定义方式
name := make(chan type, size)
其中type表示通道内的数据类型;name:通道的变量名称,size代表缓冲的长度。
二、通道的数据收发
1. 通道的数据发送
通道的发送的操作符
chan
注意,如果将数据发送至一个无缓冲的通道中,如果数据一直都没有接收,那么发送操作将持续阻塞。但是GO的编译器能够发现明显的错误,比如
ch := make(chan int) // 创建一个整型通道
ch
编译时会报错:fatal error: all goroutines are asleep - deadlock!
2.通道的数据接收
通道接收数据的操作符也是
1) 阻塞接收数据
阻塞模式接收数据时,将接收变量作为
data :=
执行该语句时将会阻塞,直到接收到数据并赋值给 data 变量。
如需要忽略接收的数据,则将data变量省略,具体格式如下:
2) 非阻塞接收数据
使用非阻塞方式从通道接收数据时,语句不会发生阻塞,格式如下:
data, ok :=
非阻塞的通道接收方法可能造成高的 CPU 占用,因此使用非常少。一般只配合select语句配合定时器做超时检测时使用。
三、channel的超时检测
Go语言没有提供直接的超时处理机制,一般使用select关键字来设置超时。Select虽然不是专为超时而设计的,却能很方便的解决超时问题,因为select的特点是只要其中有一个 case 已经完成,程序就会继续往下执行,而不会考虑其他 case 的情况。select 的用法与 switch 语言非常类似,由 select 开始一个新的选择块,每个选择条件由 case 语句来描述。
但是与switch 语句相比,select 有比较多的限制,其中最大的一条限制就是每个 case 语句里必须是一个 IO 操作,结构如下:
select {
case
// 如果chan1成功读到数据,则进行该case处理语句
case chan2
// 如果成功向chan2写入数据,则进行该case处理语句
default:
// 如果上面都没有成功,则进入default处理流程
}
比如在示例代码中,我们创建了一个用于传数据的channel,和一个用于超时退出的通道quit,并使用select来接收channel的数据,其中如果程序运行到3s时,退出通道会被置为true,这时
package main
import (
"fmt"
"time"
)
func main() {
ch := make(chan int) //设置一个传送数据的channel
quit := make(chan bool) //设置一个超时传送的channel
//新开一个协程
go func() {
for {
select {
case num :=
fmt.Println("num = ", num)
case
fmt.Println("超时")
quit
}
}
}() //别忘了()
for i := 0; i < 5; i++ {
ch
time.Sleep(time.Second)
}
fmt.Println("程序结束")
/*运行结果为
num = 0
num = 1
num = 2
num = 3
num = 4
超时
程序结束
*/
}
四、通道数据收发的注意事项
1.通道的收发操作在不同的两个 goroutine 间进行。由于通道的数据在没有接收方处理时,数据发送方会持续阻塞,因此通道的接收必定在另外一个 goroutine 中进行。
2.接收将持续阻塞直到发送方发送数据。如果接收方接收时,通道中没有发送方发送数据,接收方也会发生阻塞,直到发送方发送数据为止。
3.每次只接收一个元素。
第4节 深入理解GO语言中的channel
GO语言中的有关chan的代码位置在GOPATH\src\runtime\chan.go,阅读代码可以发现channel 内部就是一个带锁的队列。
1.基本数据结构
type hchan struct {
qcount uint // 队列中数据个数
dataqsiz uint // channel 大小
buf unsafe.Pointer // 存放数据的环形数组
elemsize uint16 // channel 中数据类型的大小
closed uint32 // 表示 channel 是否关闭
elemtype *_type // 元素数据类型
sendx uint // send 的数组索引
recvx uint // recv 的数组索引
recvq waitq // 由 recv 行为(也就是
sendq waitq // 由 send 行为 (也就是 ch
// lock protects all fields in hchan, as well as several
// fields in sudogs blocked on this channel.
//
// Do not change another G's status while holding this lock
// (in particular, do not ready a G), as this can deadlock
// with stack shrinking. lock mutex
}
如果是一个无缓冲的chan只需要用一个 lock来确保无竞争冲突。而带缓冲的chan其实就是一个环形队列,通过sendx 和 recvx 分别用来记录发送、接收的位置。
2.数据发送的实现
其实数据发送和接收的逻辑比类似,这里我们只举数据发送的例子。
func chansend(c *hchan, ep unsafe.Pointer, block bool, callerpc uintptr) bool {
if c == nil {//正确性检查
if !block {
return false
}
gopark(nil, nil, waitReasonChanSendNilChan, traceEvGoStop, 2)
throw("unreachable")
}
if debugChan {
print("chansend: chan=", c, "\n")
}
if raceenabled {
racereadpc(c.raceaddr(), callerpc, funcPC(chansend))//重置竞争标志位
}
// Fast path: check for failed non-blocking operation without acquiring the lock.
//
// After observing that the channel is not closed, we observe that the channel is
// not ready for sending. Each of these observations is a single word-sized read
// (first c.closed and second c.recvq.first or c.qcount depending on kind of channel).
// Because a closed channel cannot transition from 'ready for sending' to
// 'not ready for sending', even if the channel is closed between the two observations,
// they imply a moment between the two when the channel was both not yet closed
// and not ready for sending. We behave as if we observed the channel at that moment,
// and report that the send cannot proceed.
//
// It is okay if the reads are reordered here: if we observe that the channel is not
// ready for sending and then observe that it is not closed, that implies that the
// channel wasn't closed during the first observation.
if !block && c.closed == 0 && ((c.dataqsiz == 0 && c.recvq.first == nil) ||
(c.dataqsiz > 0 && c.qcount == c.dataqsiz)) {
return false//如果队列被关闭等情况,返回错误
}
var t0 int64
if blockprofilerate > 0 {
t0 = cputicks()
}
lock(&c.lock)
if c.closed != 0 {
unlock(&c.lock)
panic(plainError("send on closed channel"))
}
if sg := c.recvq.dequeue(); sg != nil {
// Found a waiting receiver. We pass the value we want to send
// directly to the receiver, bypassing the channel buffer (if any).
send(c, sg, ep, func() { unlock(&c.lock) }, 3)
return true
}
if c.qcount < c.dataqsiz {
// Space is available in the channel buffer. Enqueue the element to send.
qp := chanbuf(c, c.sendx)
if raceenabled {
raceacquire(qp)
racerelease(qp)
}
typedmemmove(c.elemtype, qp, ep)
c.sendx++
if c.sendx == c.dataqsiz {
c.sendx = 0
}
c.qcount++
unlock(&c.lock)
return true
}
if !block {
unlock(&c.lock)
return false
}
// Block on the channel. Some receiver will complete our operation for us.
gp := getg()
mysg := acquireSudog()
mysg.releasetime = 0
if t0 != 0 {
mysg.releasetime = -1
}
// No stack splits between assigning elem and enqueuing mysg
// on gp.waiting where copystack can find it.
mysg.elem = ep
mysg.waitlink = nil
mysg.g = gp
mysg.isSelect = false
mysg.c = c
gp.waiting = mysg
gp.param = nil
c.sendq.enqueue(mysg)
goparkunlock(&c.lock, waitReasonChanSend, traceEvGoBlockSend, 3)
// Ensure the value being sent is kept alive until the
// receiver copies it out. The sudog has a pointer to the
// stack object, but sudogs aren't considered as roots of the
// stack tracer.
KeepAlive(ep)
// someone woke us up.
if mysg != gp.waiting {
throw("G waiting list is corrupted")
}
gp.waiting = nil
if gp.param == nil {
if c.closed == 0 {
throw("chansend: spurious wakeup")
}
panic(plainError("send on closed channel"))
}
gp.param = nil
if mysg.releasetime > 0 {
blockevent(mysg.releasetime-t0, 2)
}
mysg.c = nil
releaseSudog(mysg)
return true
}
我们看到在发送数据时,首先获取lock,然后进行竞争检查、指针检查等,然后更新读写位置,记录数据,并释放锁,如果,当前hchan.buf 无可用空间,则将操作阻塞。
所以从本质上讲channel就是一个基于锁的循环队列。