文章被同步发布于个人博客,感兴趣的读者可以前往博客阅读。
前言
在Go中,有着这样一种数据类型,它为多个协程之间的通讯提供了一种便捷的通道。它就是Channel类型。Channel 是 Go 中的一个核心类型,你可以把它看成一个管道,通过它并发核心单元就可以发送或者接收数据进行通讯。
底层的实现
在Go中,channel的底层实现是一个结构体(源代码位于src/runtime/chan.go):
type hchan struct {
qcount uint // total data in the queue
dataqsiz uint // size of the circular queue
buf unsafe.Pointer // points to an array of dataqsiz elements
elemsize uint16
closed uint32
elemtype *_type // element type
sendx uint // send index
recvx uint // receive index
recvq waitq // list of recv waiters
sendq waitq // list of send waiters
// 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
}
对hchan的字段解释如下:
qcount uint: 当前队列中剩余元素个数dataqsiz uint: 环形队列长度,即缓冲区的大小,即make(chan T,N),N.buf unsafe.Pointer:环形队列指针elemsize uint16: 每个元素的大小closed uint32: 表示当前通道是否处于关闭状态。创建通道后,该字段设置为0,即通道打开; 通过调用close将其设置为1,通道关闭。elemtype *_type: 元素类型,用于数据传递过程中的赋值; sendx uint和recvx uint是环形缓冲区的状态字段,它指示缓冲区的当前索引 - 支持数组,它可以从中发送数据和接收数据。recvq waitq: 等待读消息的goroutine队列sendq waitq: 等待写消息的goroutine队列lock mutex: 互斥锁,为每个读写操作锁定通道,因为发送和接收必须是互斥操作。
而waitq的定义是一个双向链表:
type waitq struct {
first *sudog
last *sudog
}
这里存储了所有等待中的goroutine队列。
sudog是goroutine的一层封装,它是这样书写的(源代码在src/runtime/runtime2.go):
type sudog struct {
// The following fields are protected by the hchan.lock of the
// channel this sudog is blocking on. shrinkstack depends on
// this for sudogs involved in channel ops.
g *g
next *sudog
prev *sudog
elem unsafe.Pointer // data element (may point to stack)
// The following fields are never accessed concurrently.
// For channels, waitlink is only accessed by g.
// For semaphores, all fields (including the ones above)
// are only accessed when holding a semaRoot lock.
acquiretime int64
releasetime int64
ticket uint32
// isSelect indicates g is participating in a select, so
// g.selectDone must be CAS'd to win the wake-up race.
isSelect bool
// success indicates whether communication over channel c
// succeeded. It is true if the goroutine was awoken because a
// value was delivered over channel c, and false if awoken
// because c was closed.
success bool
parent *sudog // semaRoot binary tree
waitlink *sudog // g.waiting list or semaRoot
waittail *sudog // semaRoot
c *hchan // channel
}
此处的g就是goroutine,而elem便是数据存放区域的指针,在这片区域内存储的数据便是channel用来读取或发送的。
而整个Channel的图例大概为这样:(笔者是网上找的图片,下面把hchan拼错了)
创建chan
和map、slice一样,chan 在使用前是需要用make函数开辟新的内存空间的:
c := make(chan <type>,[cap])
<type>表示chan的传输类型;[cap]表示分配给chan的缓冲区大小,可以忽略。
而make函数会调用在runtime包内的makeChan函数:
const (
maxAlign = 8
hchanSize = unsafe.Sizeof(hchan{}) + uintptr(-int(unsafe.Sizeof(hchan{}))&(maxAlign-1))
)
func makechan(t *chantype, size int) *hchan {
elem := t.elem
// 略去检查代码
...
//计算需要分配的buf空间
mem, overflow := math.MulUintptr(elem.size, uintptr(size))
if overflow || mem > maxAlloc-hchanSize || size < 0 {
panic(plainError("makechan: size out of range"))
}
var c *hchan
switch {
case mem == 0:
// chan的size或者元素的size是0,不必创建buf
c = (*hchan)(mallocgc(hchanSize, nil, true))
// Race detector
c.buf = c.raceaddr()
case elem.ptrdata == 0:
// 元素不是指针,分配一块连续的内存给hchan数据结构和buf
c = (*hchan)(mallocgc(hchanSize+mem, nil, true))
// 表示hchan后面在内存里紧跟着就是buf
c.buf = add(unsafe.Pointer(c), hchanSize)
default:
// 元素包含指针,那么单独分配buf
c = new(hchan)
c.buf = mallocgc(mem, elem, true)
}
c.elemsize = uint16(elem.size)
c.elemtype = elem
c.dataqsiz = uint(size)
return c
}
缓冲区
前文我们说过,在创建channel时我们可以去给它一块缓冲区。根据缓冲区,我们可以将chan分为无缓冲区的channel和有缓冲区的channel。
-
无缓冲区的channel:
对无缓冲区的 channel 发送 / 接收数据时,无缓冲区的 channel 会发送阻塞。当程序中只有对该 channel 发送或接收数据操作时,该程序会发生死锁。因此在程序中使用无缓冲区的 channel 时,必须同时存在发送和接收操作 (可以在 goroutine 中实现), 所以无缓冲区的 channel 又被称为同步 channel。
-
有缓冲区的chan:
有缓冲区的 channel 类似于一个队列。当缓冲区未满时,向 channel 中发送数据不会阻塞。当缓冲区满时,发送数据操作将被阻塞,直到有其它 goroutine 从中读取消息。
数据的交互:
在Go中,我们可以使用<-、->操作符来向某个channel发送/接受数据。而数据的去向则根据箭头的方向走。
c := make(chan int)
//向chan发送一个int
c <- 5
//从chan内读取一个int
b := <- c
那,如果chan里面没有东西或者塞满了呢?
阻塞
前面我们说过,channel是配合协程去使用的。
当某个协程向channel内发送数据时,若channel满了,则该协程会先挂起,等到channel内部的数据有空位时再执行发送操作;同样的,当某个协程从channel内读取数据时,若channel空了也会触发挂起操作,直到channel的内部有数据了再读取执行后续操作。例如下面的代码:
package main
import (
"fmt"
"time"
)
func main() {
c := make(chan int, 3)
c <- 1
c <- 2
c <- 3
go func() {
fmt.Println("我在尝试发送数据")
c <- 4
fmt.Println("我发完了")
}()
go func() {
fmt.Println("我在尝试读取数据")
_ = <-c
fmt.Println("我读完了")
}()
time.Sleep(5 * time.Second)
}
输出结果:
我在尝试发送数据
我在尝试读取数据
我读完了
我发完了
可以观察到,发送的协程会先挂起,直到读取的协程读出一个数字时才会继续执行发送操作。
关闭
go 提供了内置的 close 函数对 channel 进行关闭操作。
ch := make(chan int)
close(ch)
有关 channel 的关闭,有如下注意事项:
- 关闭未初始化的 channel (nil) 会产生 panic
- 关闭已关闭的 channel 会产生 panic
- 向已关闭的 channel 中发送数据会产生 panic
- 从一个已关闭的 channel 中读取消息不会产生 panic 和阻塞。如果已关闭的 channel 的缓冲区还有数据,则可以正常读取,并返回值为
true的 ok-idiom. 否则,返回 channel 的默认值和 false 的 ok-idiom. - 要将已关闭的 channel 中的数据全部读取出来,可以使用 for-range 方式进行读取。遍历结束后,channel 缓冲区数据为空。此时再进行读取,会返回 channel 的默认值和 false 的 ok-idiom。
import "fmt"
func main() {
ch := make(chan int, 1)
ch <- 1
close(ch)
for value := range ch {
fmt.Println(value)
}
val, ok := <-ch
fmt.Println(val, ok)
}
select
select的用法和switch相似,但它可以用来监听channel的IO操作:
select {
case <- chan1:
// 如果chan1成功读到数据,则进行该case处理语句
case chan2 <- 1:
// 如果成功向chan2写入数据,则进行该case处理语句
default:
// 如果上面都没有成功,则进入default处理流程
在select的使用中注意两个要点:
- 如果没有
default分支,select会阻塞在多个channel上,对多个channel的读/写事件进行监控。 - 如果有一个或多个IO操作可以完成,则Go运行时系统会随机的选择一个执行,否则的话,如果有
default分支,则执行default分支语句,如果连default都没有,则select语句会一直阻塞,直到至少有一个IO操作可以进行。
实例
1. 阻塞主routine
package main
import (
"fmt"
"time"
)
func main() {
c := make(chan int) // Allocate a channel.
// Start the sort in a goroutine; when it completes, signal on the channel.
go func() {
fmt.Println("Please wait for a while......")
for i := 0; i < 5; i++ {
fmt.Print(".")
time.Sleep(time.Second)
}
c <- 1 // Send a signal; value does not matter.
}()
<-c
fmt.Println("Done")
}
由于主routine需要读取来自c的数据,因此主routine会触发阻塞,直到子routine等待5秒后发送一个信号,主routine才会继续运行。
2. 配合select
select 可以同时监听多个 channel 的消息状态.
当其中一个 case 语句非阻塞,则执行对应 case 语句中的内容。若有多个 case 语句非阻塞,则随机挑选一个 case 语句中的内容执行.
若所有 case 语句均处于阻塞状态且定义了 default 语句,则执行 default 语句中的内容。否则,一直阻塞
import (
"fmt"
"sync"
)
func main() {
var wg sync.WaitGroup
wg.Add(2)
ach, bch := make(chan int), make(chan int)
// 消费者 goroutine
go func(wg *sync.WaitGroup, a, b <-chan int) {
defer wg.Done()
var (
name string
x int
ok bool
)
for {
select {
case x, ok = <-a:
name = "a"
case x, ok = <-b:
name = "b"
}
if !ok {
// 如果没有数据发送,则跳出循环
return
}
fmt.Println(name, x)
}
}(&wg, ach, bch)
// 生产者 goroutine
go func(wg *sync.WaitGroup, a, b chan<- int) {
defer wg.Done()
defer close(a)
defer close(b)
for i := 0; i < 10; i++ {
select {
case a <- i:
case b <- i * 10:
}
}
}(&wg, ach, bch)
wg.Wait()
}
上述代码,分别创建了生产者和消费者的 goroutine, 生产者会随机从 a b channel 中随机挑选一个发送消息,而消费者使用一个 for 循环来监控 a b channel, 当 a b 其中一个接收到数据时,则指定对应内容。如果没有数据,则跳出循环,结束 goroutine。