channel是什么?
前言:说channel不得不说一下这个并发谚语经典:
Don’t communicate by sharing memory; share memory by communicating.
不要通过共享内存来通信,而应该通过通信来共享内存。这是作为 Go 语言的主要创造者之一的Rob Pike的至理名言,这也充分体现了Go言最重要的编程理念。而Channel恰恰是后半句话的完美实现,我们可以利用通道在多个GoRoutine之间传递数据。(这也是Channel最重要的使用场景)
这句话常想常新
1. channel 的概念
channel 是一个通道,用于端到端的数据传输,这有点像我们平常使用的消息队列,只不过 channel 的发送方和接受方是 goroutine 对象,属于内存级别的通信。
这里涉及到了 goroutine 概念,goroutine 是轻量级的协程,有属于自己的栈空间。 我们可以把它理解为线程,只不过 goroutine 的性能开销很小,并且在用户态上实现了属于自己的调度模型。
传统的线程通信有很多方式,像内存共享、信号量等。其中内存共享实现较为简单,只需要对变量进行并发控制,加锁即可。但这种在后续业务逐渐复杂时,将很难维护,耦合性也比较强。
后来提出了 CSP 模型,即在通信双方抽象出中间层,数据的流转由中间层来控制,通信双方只负责数据的发送和接收,从而实现了数据的共享,这就是所谓的通过通信来共享内存。 channel 就是按这个模型来实现的。
channel 在多并发操作里是属于协程安全的,并且遵循了 FIFO 特性。即先执行读取的 goroutine 会先获取到数据,先发送数据的 goroutine 会先输入数据。
另外,channel 的使用将会引起 Go runtime 的调度调用,会有阻塞和唤起 goroutine 的情况产生。
和其他结构(切片,map,结构体)相比的优势:并发安全
2. 通道类型及其使用
2.1. 非缓冲通道与缓冲通道
首先channel是一个单向通道,明确这一点后在看下面
非缓冲通道
var ch = make(chan int) // 声明一个非缓冲 channel
非缓冲通道,通常是用来进行同步数据操作,日常开发中用到的地方不多。
缓冲通道
- 带缓冲的 Channel 声明:带缓冲的 channel 可以在一定容量内缓存数据,只有当缓冲区满时发送操作才会阻塞。
var ch = make(chan int, 10) // 声明一个容量为 10 的整数类型的带缓冲 channel
2.2 Channel的基础使用
2.2.1Channel使用
channel 的正常读写
往一个 channel 发送数据,可以这样写:
ch := make(chan int)
ch <- 1
对应的读操作:
data <- ch
当我们不再使用 channel 的时候,可以对其进行关闭:
close(ch)
● 如果关闭后的 channel 没有数据可读取时,将得到零值,即对应类型的默认值。
● 为了能知道当前 channel 是否被关闭,可以使用下面的写法来判断。
Channel中的for用法
a. 如果chs中没有任何数据,那么程序会阻塞在For这一行,直到有数据进来为止。
b. 如果chs已经被关闭了,那么程序会把所有元素都遍历完,再跳出循环。
c. 如果chs是nil,那么程序会永远阻塞在For这一行,不会往下执行。
if v, ok := <-ch; !ok {
fmt.Println(“channel 已关闭,读取不到数据”)
}
● 还可以使用下面的写法不断的获取 channel 里的数据:
for data := range ch {
// get data dosomething
}
Channel中的Select用法
d. 可读可写
e. select调度器
f. 在写程序时,有时并不单单只会和一个 goroutine 通信,当我们要进行多 goroutine 通信时,则会使用 select 写法来管理多个 channel 的通信数据:
ch1 := make(chan int)
ch2 := make(chan int)
go func() {
if !ok { // 某些原因,设置 ch1 为 nil
ch1 = nil
}
}()
for {
select {
case <-ch1: // 当 ch1 被设置为 nil 后,将不会到达此分支了。
doSomething1()
case <-ch2:
doSomething2()
}
}
ch1 := make(chan struct{})
ch2 := make(chan struct{})
// ch1, ch2 发送数据
go sendCh1(ch1)
go sendCh1(ch2)
// channel 数据接受处理
for {
select {
case <-ch1:
doSomething1()
case <-ch2:
doSomething2()
}
}
2.2.2 Channel使用问题
- 两个携程等待接收一个信道,向信道传一个值的话谁获得这个值?
a. 先来先得
读操作
我们以取一个元素为例,当我们操作Channel的时候实际上有这几个步骤:
找到元素->取元素的副本->将副本交给接收方->删除Channel中这个元素 我们最终能收到的就是这一串操作要么成功,要么失败,不会有中间态。这是Channel是并发安全的一个体现。
使用 channel 时我们还可以控制 channel 只读只写操作:
func readChan(ch <-chan int){
// chan 只允许被读
}
func main(){
ch := make(chan int)
readChan(ch)
}
反之,如果要求只写操作,则可以这样:
func writeChan(ch chan<- int){
// chan 只允许被写
}
3. 底层代码
3.1 底层结构
3.1.1hchsn结构体
type hchan struct {
qcount uint//当前通道中的元素个数。
dataqsiz uint//当前循环队列的长度。
buf unsafe.Pointer //指向缓冲区的指针 环形数组的关键
elemsize uint16//通道元素的大小 信道元素类型所需内存的大小不同,
closed uint32//是否关闭 elemtype *_type //通道元素的类型 重复关闭会panic
sendx uint//发送操作处理到的位置
recvx uint//接收操作处理到的位置
recvq waitq //被阻塞的接收操作队列 链表结构
sendq waitq //被阻塞的发送操作队列 链表结构
lock mutex //互斥锁
}
3.1.2 阻塞的携程队列链表
type waitq struct {
first *sudog //链表头节点
last *sudog //链表尾节点
}
阻塞携程元素
typw sudog struct {
g *g //指向gorunting
next *sudog//下一个节点
prev *sudog//上一个节点
elem unsafe.Pointer//data element(may point to stack)
isSelect bool//代表着她所在的goruneting是否处于select多路复用的状态下
c *hchan//指向她所从属的channel
}
3.1.3 构造器函数
make构造两种信道的方法
func makechan(t *chantype, size int) *hchan {
elem := t.elem
// compiler checks this but be safe.
if elem.size >= 1<<16 {
throw("makechan: invalid channel element type")
}
if hchanSize%maxAlign != 0 || elem.align > maxAlign {
throw("makechan: bad alignment")
}
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:
// Queue or element size is zero.
c = (*hchan)(mallocgc(hchanSize, nil, true))
// Race detector uses this location for synchronization.
c.buf = c.raceaddr()
case elem.ptrdata == 0:
// Elements do not contain pointers.
// Allocate hchan and buf in one call.
c = (*hchan)(mallocgc(hchanSize+mem, nil, true))
c.buf = add(unsafe.Pointer(c), hchanSize)
default:
// Elements contain pointers.
c = new(hchan)
c.buf = mallocgc(mem, elem, true)
}
c.elemsize = uint16(elem.size)
c.elemtype = elem
c.dataqsiz = uint(size)
lockInit(&c.lock, lockRankHchan)
if debugChan {
print("makechan: chan=", c, "; elemsize=", elem.size, "; dataqsiz=", size, "\n")
}
return c
}
3.2 发送send
两种异常:
3.2.1 case1:
if c == nil {//c是channel,为nil即没有正确初始化
if !block {//block 参数用于指示是否要在当前操作被阻塞时等待,
//如果为 false,说明不希望被阻塞,直接返回 false 表示发送失败。
return false
}
gopark(nil, nil, waitReasonChanSendNilChan, traceEvGoStop, 2)
//gopark 是 Go 语言运行时系统用来使 goroutine 进入休眠状态的函数。
throw("unreachable")//抛出异常
}//向一个没有初始话的channel传数据
var ch chan int//没有初始化的声明,直接send这种channel会让gorountine被动阻塞,导致死锁,并抛出一个异常阻塞
ch := make(chan int,10)//正常声明且初始化
3.2.2case2:
向一个关闭的channel进行send时会直接抛出一个错误
3.2.3具体chansend代码
func chansend(c *hchan, ep unsafe.Pointer, block bool, callerpc uintptr) bool {
//c信道
//ep带send的数据指针,
//gorountine是否可以阻塞
if c == nil {
if !block {
return false
}
gopark(nil, nil, waitReasonChanSendNilChan, traceEvGoStop, 2)
throw("unreachable")//抛出异常
}//向一个没有初始话的channel传数据
if debugChan {//是否在调试
print("chansend: chan=", c, "\n")
}//打印channel的地址
if raceenabled {
racereadpc(c.raceaddr(), callerpc, abi.FuncPCABIInternal(chansend))
}
//如果 raceenabled 为真,表示编译时开启了数据竞争检测(Race Detector),
//则会调用 racereadpc 函数。
//Race Detector 用于检测并报告多个 goroutine 之间的数据竞争情况。
//racereadpc 用于标记一个读取操作的位置,
//以便 Race Detector 可以检测读取操作是否与其他写入操作存在竞争。
//这里会标记当前 channel 的地址和发送操作的位置。
if !block && c.closed == 0 && full(c) {//send的rountine不允许阻塞,且通道没有关闭,且通道已满
return false
}
var t0 int64
if blockprofilerate > 0 {
t0 = cputicks()
}
//当 blockprofilerate 大于 0 时,
//代码执行 cputicks() 并将返回的时钟周期数赋值给变量 t0。
//这就记录了当前的时间作为发送操作的起始时间。进行阻塞对于性能的分析
lock(&c.lock)//只能有一个gorountine进行send操作
if c.closed != 0 {
unlock(&c.lock)
panic(plainError("send on closed channel"))
}//channel被关闭直接抛出错误
if sg := c.recvq.dequeue(); sg != nil {//如果有队列
send(c, sg, ep, func() { unlock(&c.lock) }, 3)//发送
return true
}
//c:信道
//crevq:等待接收的携程队列
//dequeue()取出第一个携程
//qcount缓冲区内数据数量
//dataqsiz缓冲区总大小
if c.qcount < c.dataqsiz {//channel的缓冲区没有满
qp := chanbuf(c, c.sendx)//获取缓冲区的send位置
if raceenabled {
racenotify(c, c.sendx, nil)
}//数据竞争有关
typedmemmove(c.elemtype, qp, ep)//将数据放进channel中qp的位置
c.sendx++//send位置++
if c.sendx == c.dataqsiz {
c.sendx = 0//如果send指针到尾部,就重置为0
}
c.qcount++//增加缓冲区数据计数
unlock(&c.lock)
return true
}
if !block {//不阻塞
unlock(&c.lock)
return false
}
gp := getg()//获取当前正在执行的 goroutine 的 g 结构体
mysg := acquireSudog()//使用 acquireSudog() 函数获取一个空闲的 sudog 结构体,
//用于表示一个等待操作的 goroutine。sudog 是 Go 语言运行时系统用来管理等待和唤醒的结构
mysg.releasetime = 0//首先,将 mysg 的 releasetime 字段设置为 0,表示释放时间尚未记录。
if t0 != 0 {
mysg.releasetime = -1
}//不重点讲
mysg.elem = ep//获取数据指针
mysg.waitlink = nil//这个字段用于构建等待队列的链表结构。
mysg.g = gp//将当前 goroutine(gp)赋值给 mysg 的 g 字段,
//表示这个 sudog 结构关联到当前的 goroutine。
mysg.isSelect = false//表示这个 sudog 结构是用于等待发送操作。
mysg.c = c//获取当前channel
//设置gp携程
gp.waiting = mysg//表示gorounte正在等待
gp.param = nil//用于清空可能存储的参数???
c.sendq.enqueue(mysg)//将sudog添加在信道的发送阻塞队列
gp.parkingOnChan.Store(true)//将当前 goroutine 的状态标记为正在阻塞在 channel 上。
gopark(chanparkcommit, unsafe.Pointer(&c.lock), waitReasonChanSend, traceEvGoBlockSend, 2)
//让gorountine休眠
KeepAlive(ep)//保持 ep 存活,以确保在发送操作完成后数据不会被提前回收。
if mysg != gp.waiting {//
throw("G waiting list is corrupted")
}//检查等待的 sudog 结构是否与当前 goroutine 的 waiting 字段相符,
//如果不相符,说明等待列表被损坏,抛出异常
gp.waiting = nil//如果为nil不再等待
gp.activeStackChans = false//表示当前 goroutine 不再处于等待 channel 的状态。
closed := !mysg.success//
gp.param = nil
if mysg.releasetime > 0 {
blockevent(mysg.releasetime-t0, 2)
}
mysg.c = nil
releaseSudog(mysg)
if closed {
if c.closed == 0 {
throw("chansend: spurious wakeup")
}
panic(plainError("send on closed channel"))
}
return true
}
3.3.1 接收recv
func chanrecv(c *hchan, ep unsafe.Pointer, block bool) (selected, received bool) {
//c 通道
//ep ep 是接收数据的目标地址
//block 表示是否阻塞等待接收值
//selected 表示通道接收操作是否成功。
//如果 select 为 true,则表示成功进行了通道接收操作,不论是否实际接收到了一个值。
//如果 select 为 false,则表示通道接收操作失败,可能是因为通道已关闭或者非阻塞模式下通道为空。
//received 表示是否实际接收到了一个值。
//如果 received 为 true,则表示成功接收到了一个值。
//如果 received 为 false,则表示虽然成功进行了通道接收操作,但没有实际接收到一个值,
//通常是因为接收地址为 nil 或者通道中的数据类型不匹配等原因。
//selected 和 received 的默认值为 false
if debugChan {//测试模式下使用
print("chanrecv: chan=", c, "\n")
}
if c == nil {
if !block {
return
}//可以不用导致异常
gopark(nil, nil, waitReasonChanReceiveNilChan, traceEvGoStop, 2)
throw("unreachable")
}
if !block && empty(c) {//gorune不能阻塞且通道为空
if atomic.Load(&c.closed) == 0 {//通道关闭的话
return//直接返回
}
if empty(c) {//它再次检查通道 c 是否为空。如果通道为空,这表示在上面的检查中通道状态没有发生变化,仍然为空。
if raceenabled {//数据竞争检查不重要
raceacquire(c.raceaddr())
}
if ep != nil {
typedmemclr(c.elemtype, ep)//使ep为nil
}
return true, false//表示接收操作完成,但是没有实际接收到值
}
}
var t0 int64//用以性能分析
if blockprofilerate > 0 {
t0 = cputicks()
}
lock(&c.lock)//上锁
if c.closed != 0 {//通道关闭的话
if c.qcount == 0 {//且缓存没有数据的话
if raceenabled {//数据竞争
raceacquire(c.raceaddr())
}
unlock(&c.lock)//解锁
if ep != nil {//让ep为nil
typedmemclr(c.elemtype, ep)
}
return true, false//表示接收操作完成,但是没有实际接收到值
}
} else {//通道没有关闭的话
if sg := c.sendq.dequeue(); sg != nil {//如果发送队列不为nil
recv(c, sg, ep, func() { unlock(&c.lock) }, 3)
return true, true
}
//使用 recv 函数来完成接收。
//它将通道 c、发送操作 sg、接收地址 ep 以及一个解锁函数作为参数传递给 recv 函数。
//接收操作会尝试从发送操作中获取值并将其写入接收地址 ep
}
//dequeue 方法会尝试从发送队列中获取一个发送操作
if c.qcount > 0 {//缓冲区内有数据
qp := chanbuf(c, c.recvx)//获取缓冲区的数值
if raceenabled {//数据经常不重要
racenotify(c, c.recvx, nil)
}
if ep != nil {
typedmemmove(c.elemtype, ep, qp)
//则将缓冲区中的数据移动到接收地址 ep 中。这表示成功接收到了数据,并将其存储到指定的接收地址。
}
typedmemclr(c.elemtype, qp)
//清除缓冲区中的数据,因为数据已经被成功接收。
c.recvx++//接收指针+1
if c.recvx == c.dataqsiz {
c.recvx = 0
}//满则重置
c.qcount--//缓存区数量-1
unlock(&c.lock)解锁
return true, true
}//如果ep = nil,也会清楚qp在缓存区的数据
if !block {//不能阻塞则返回
unlock(&c.lock)
return false, false
}
//下面的全部都是阻塞
gp := getg()
mysg := acquireSudog()
mysg.releasetime = 0
if t0 != 0 {
mysg.releasetime = -1
}
mysg.elem = ep
mysg.waitlink = nil
gp.waiting = mysg
mysg.g = gp
mysg.isSelect = false
mysg.c = c
gp.param = nil
c.recvq.enqueue(mysg)
gp.parkingOnChan.Store(true)
gopark(chanparkcommit, unsafe.Pointer(&c.lock), waitReasonChanReceive, traceEvGoBlockRecv, 2)
if mysg != gp.waiting {
throw("G waiting list is corrupted")
}
gp.waiting = nil
gp.activeStackChans = false
if mysg.releasetime > 0 {
blockevent(mysg.releasetime-t0, 2)
}
success := mysg.success
gp.param = nil
mysg.c = nil
releaseSudog(mysg)
return true, success
}
3.3.2具体完成的接收操作
func recv(c *hchan, sg *sudog, ep unsafe.Pointer, unlockf func(), skip int) {
//c channel
//sg 携程
//ep 等待接收的指针
//unlockf 解锁函数
//skip 传的定值3
if c.dataqsiz == 0 {//如果为 0,表示通道是非缓冲通道
if raceenabled {//竞态检测,不重要
racesync(c, sg)
}
if ep != nil {
// copy data from sender
recvDirect(c.elemtype, sg, ep)//从发送方(sg)复制数据到接收方(ep)
}
} else {
// Queue is full. Take the item at the
// head of the queue. Make the sender enqueue
// its item at the tail of the queue. Since the
// queue is full, those are both the same slot.
qp := chanbuf(c, c.recvx)//它获取通道缓冲中的头部元素qp
if raceenabled {//竞态检测,不重要
racenotify(c, c.recvx, nil)
racenotify(c, c.recvx, sg)
}
// copy data from queue to receiver
if ep != nil {
typedmemmove(c.elemtype, ep, qp)//从队列中复制数据到接收方(ep)。
}
// copy data from sender to queue
typedmemmove(c.elemtype, qp, sg.elem)发送操作的值放在缓存区
c.recvx++
if c.recvx == c.dataqsiz {
c.recvx = 0
}
c.sendx = c.recvx // c.sendx = (c.sendx+1) % c.dataqsiz
}//队列中的数据已经被接收,队列空出来了
sg.elem = nil//表示携程接收成功,
gp := sg.g//获取等待接收的携程
unlockf()//解锁通道
gp.param = unsafe.Pointer(sg)
sg.success = true//将 sg.success 设置为 true,表示接收成功。
if sg.releasetime != 0 {
sg.releasetime = cputicks()
}
goready(gp, skip+1)
}
3.4关闭close
func closechan(c *hchan) {
if c == nil {
panic(plainError("close of nil channel"))
}//为nil直接panic
lock(&c.lock)//上锁
if c.closed != 0 {
unlock(&c.lock)//解锁
panic(plainError("close of closed channel"))//异常
}
if raceenabled {//竞态检测
callerpc := getcallerpc()
racewritepc(c.raceaddr(), callerpc, abi.FuncPCABIInternal(closechan))
racerelease(c.raceaddr())
}
c.closed = 1//表示已经关闭,相当于伪关闭
var glist gList//用于存储等待读取或写入通道的 goroutine。
// release all readers
for {
sg := c.recvq.dequeue()//释放所有等待接收的goroutinr
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 = unsafe.Pointer(sg)
sg.success = false
if raceenabled {//竞争检测
raceacquireg(gp, c.raceaddr())
}
glist.push(gp)
}
// release all writers (they will panic)
for {
sg := c.sendq.dequeue()//等待写入的goroutine
if sg == nil {
break//没有就停止
}
sg.elem = nil//避免内存泄漏。
if sg.releasetime != 0 {
sg.releasetime = cputicks()
}
gp := sg.g
gp.param = unsafe.Pointer(sg)
sg.success = false
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()//将goroutine取出
gp.schedlink = 0//这里将其设置为 0,表示不再具有与调度相关的链接。
goready(gp, 3)
}//将 gp 标记为可调度状态,并将其放入调度队列中以等待执行。
//3 是一个可选的参数,可能用于指示在调度时的优先级或其他调度相关信息。
}
4. 情景案例
4.1 close部分
关闭一个有发送阻塞的channel会直接报错
关闭一个有接收阻塞的channel会解开阻塞,并且接收的值为零值
channel 的 deadlock
前面提到过,往 channel 里读写数据时是有可能被阻塞住的,一旦被阻塞,则需要其他的 goroutine 执行对应的读写操作,才能解除阻塞状态。
然而,阻塞后一直没能发生调度行为,没有可用的 goroutine 可执行,则会一直卡在这个地方,程序就失去执行意义了。此时 Go 就会报 deadlock 错误,如下代码:
func main() {
ch := make(chan int)
<-ch
// 执行后将 panic:
// fatal error: all goroutines are asleep - deadlock!
}
因此,在使用 channel 时要注意 goroutine 的一发一取,避免 goroutine 永久阻塞!
4.2 阻塞与非阻塞模式
读取一个0值如何判断是本身传递的值就是0,还是关闭后为0
if val,ok := <- ch;ok {
//open
} else {
//close
}
//使用时
ch := make(chan int, 2)
got1 := <- ch chanrecv1
got2,ok := <- ch chanrecv2
//实现上述功能的原因是,两种格式下,读channel方法会被会变成不同的方法
func chanrecv1(c *hchan, elem unsage.Pointer) {
chanrecv(c,elem,true)
}
//go:nosplit
func chanrecv2(c *hchan,elem unsafe.Pointer) (received bool) {
_, received = chanrecv(c,elem, true)
return
}
4.3 读取一个已关闭的channel会发生什么
4.3.1 读一个已关闭的channel
如何缓存区有数值,则正常读取,如果缓存区没值或者没有缓冲区,则返回一个channel元素类型的零值和一个bool类型的false
4.3.2 写入一个已关闭的channel
直接报panic,写入一个已关闭的channel是非法的,
5.总结
最后提问:
- 如何设置携程的block
- 如何唤醒阻塞的goruntine(具体代码)
- 发送阻塞的数据进不进入信道
- 还有其他问题吗?
最后总结:
channel的问题并不难,主要的难点就是并发这一个,如何让携程阻塞的,唤醒的这一大块是难点,相比map,基础的channel还是非常简单的