我正在参加「掘金·启航计划」
1、前言
channel是在语言层面提供的goroutine间的通信方式,channel主要用于进程内各goroutine间的通信,如果需要跨进程通信,建议使用分布式系统方法解决。
2、chan数据结构
channel由队列、类型信息、goroutine等待队列组成。
在src/runtime/chan.go
包中定义了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 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不允许并发读写
}
2.1、环形队列
chan内部实现了一个环形队列作为其缓冲区,队列的长度是创建chan时指定的。
- dataqsiz 指示了队列长度为6;
- buf指向队列的内存,队列中还剩余两个元素;
- qcount表示队列中还有两个元素;
- sendx指示后续写入的数据存储的位置,取值[0,6)
- recvx指示从该位置读取数据,取值[0, 6)
2.2、等待队列
从channel读数据,如果channel缓冲区为空或者没有缓冲区,当前goroutine会被阻塞。
向channel写数据,如果channel缓冲区已满或者没有缓冲区,当前goroutine会被阻塞。
被阻塞的goroutine将会挂在channel的等待队列中:
- 因读阻塞的goroutine会被向channel写入数据的goroutine唤醒;
- 因写阻塞的goroutine会被从channel读数据的goroutine唤醒;
示例:
一个没有缓冲区的channel,有几个goroutine阻塞等待读数据。
2.3、类型信息
一个channel只能传递一种类型的值,类型信息存储在hchan数据结构中。
- elemtype代表类型,用于数据传递过程的赋值;
- elemsize代表类型大小,用于在buf中定位元素位置;
2.4、锁
一个channel同时仅被一个goroutine读写。
3、channel读写
3.1、创建channel
创建channel实际上是初始化hchan结构,其中类型信息和缓冲区长度由make语句传入,buf的大小与元素大小和缓冲区长度决定。
func makechan(t *chantype, size int) *hchan {
var c *hchan
c = new(hchan)
c.buf = malloc(元素类型大小*size)
c.elemsize = uint16(elem.size) //元素类型大小
c.elemtype = elem //元素类型
c.dataqsiz = uint(size)
lockInit(&c.lock, lockRankHchan)
return c
}
3.2、向channel写数据
向一个channel中写数据流程如下:
- 写入数据的时候,若recvq 队列为空,且循环队列有空位,那么就直接将数据写入到 循环队列的队尾 即可
- 若recvq 队列为空,且循环队列无空位,则将当前的协程放到sendq等待队列中进行阻塞,等待被唤醒,当被唤醒的时候,需要写入的数据,已经被读取出来,且已经完成了写入操作
- 若recvq 队列为不为空,那么可以说明循环队列中没有数据,或者循环队列是空的,即没有缓冲区(向无缓冲的通道写入数据),此时,直接将recvq等待队列中取出一个G,写入数据,唤醒G,完成写入操作
3.3、从channel读数据
从一个channel读数据简单过程如下:
- 若sendq为空,且循环队列无元素的时候,那就将当前的协程加入recvq等待队列,把recvq等待队列对头的一个协程取出来,唤醒,读取数据
- 若sendq为空,且循环队列有元素的时候,直接读取循环队列中的数据即可
- 若sendq有数据,且循环队列有元素的时候,直接读取循环队列中的数据即可,且把sendq队列取一个G放到循环队列中,进行补充
- 若sendq有数据,且循环队列无元素的时候,则从sendq取出一个G,并且唤醒他,进行数据读取操作
3.4、关闭channel
关闭channel时会把recvq的G全部唤醒,本该写入G的数据为nil,把sendq中的G全部唤醒,但这些G会panic。
func closechan(c *hchan) {
if c == nil {
panic(plainError("close of nil channel"))
}
lock(&c.lock)
if c.closed != 0 {
unlock(&c.lock)
panic(plainError("close of closed channel"))
}
if raceenabled {
callerpc := getcallerpc()
racewritepc(c.raceaddr(), callerpc, funcPC(closechan))
racerelease(c.raceaddr())
}
c.closed = 1
var glist gList
// release all readers
for {
sg := c.recvq.dequeue()
if sg == nil {
break
}
if sg.elem != nil {
typedmemclr(c.elemtype, sg.elem)
sg.elem = nil
}
if sg.releasetime != 0 {
sg.releasetime = cputicks()
}
gp := sg.g
gp.param = nil
if raceenabled {
raceacquireg(gp, c.raceaddr())
}
glist.push(gp)
}
// release all writers (they will panic)
for {
sg := c.sendq.dequeue()
if sg == nil {
break
}
sg.elem = nil
if sg.releasetime != 0 {
sg.releasetime = cputicks()
}
gp := sg.g
gp.param = nil
if raceenabled {
raceacquireg(gp, c.raceaddr())
}
glist.push(gp)
}
unlock(&c.lock)
// Ready all Gs now that we've dropped the channel lock.
for !glist.empty() {
gp := glist.pop()
gp.schedlink = 0
goready(gp, 3)
}
}
除此之外,出现panic的场景还有:
- 关闭值为nil的channel
- 关闭已经被关闭的channel
- 向已经关闭的channel写数据
4、常见用法
4.1、select
GO 里面Chan 一般会和 select 搭配使用,直接上代码
package main
import (
"fmt"
"time"
)
func main() {
//创建两个int类型的通道,缓冲区为10
chan1 := make(chan int, 10)
chan2 := make(chan int, 10)
//向chan1写入数据
go func() {
i := 0
for {
chan1 <- i
i++
time.Sleep(time.Second)
}
}()
//向chan2写入数据
go func() {
i := 0
for {
chan2 <- i
i++
time.Sleep(time.Second)
}
}()
//读取数据
for {
select {
case num := <-chan1:
fmt.Printf("chan1读取数据:%d\n", num)
case num := <-chan2:
fmt.Printf("chan2读取数据:%d\n", num)
default:
fmt.Println("未读取到数据")
time.Sleep(time.Second)
}
}
}
从运行结果来看,select 监控的 2个 通道,读取到的数据是随机的