什么是CSP?
描述并发系统交互的模型,在Go中基于goroutine和channel实现。
通道的作用?
停止信号
定时执行任务
select{
case<-time.Tick(time.Second);
case<-s.stopc:
return
}
解耦生产者和消费者
func main() {
taskCh:=make(chan int,100)
go work(taskCh)
for i:=0;i<10;i++{
taskCh<-i
}
time.Sleep(time.Second*3)
}
func work(taskCh <-chan int) {
const N=5
for i:=0;i<N;i++{
go func (id int) {
for {
task:=<-taskCh
fmt.Printf("task:%d,id:%d\n",task,id)
time.Sleep(time.Second)
}
}(i)
}
}
控制并发数
token:=make(chan int,10)
for _,w:=range work{
go func () {
token<-1
w()
<-token
}()
}
数据结构
type hchan struct {
// 队列中目前的元素计数
qcount uint
// 环形队列的总大小
dataqsiz uint
// 指向循环数组
buf unsafe.Pointer
// 元素的大小
elemsize uint16
// 是否已被关闭
closed uint32
// runtime._type,代表 channel 中的元素类型的 runtime 结构体
elemtype *_type
// 发送索引
sendx uint
// 接收索引
recvx uint
// 接收 goroutine 对应的 sudog 队列
recvq waitq
// 发送 goroutine 对应的 sudog 队列
sendq waitq
//保证读写channel过程为原子操作
lock mutex
}
创建(makechan)
func makechan(t *chantype, size int) *hchan {
elem := t.elem
//检查元素和缓冲区的大小
if elem.size >= 1<<16 {
throw("makechan: invalid channel element type")
}
if hchanSize%maxAlign != 0 || elem.align > maxAlign {
throw("makechan: bad alignment")
}
if size < 0 || uintptr(size) > maxSliceCap(elem.size) || uintptr(size)*elem.size > _MaxMem-hchanSize {
panic(plainError("makechan: size out of range"))
}
var c *hchan
switch {
case size == 0 || elem.size == 0:
//如果无缓冲区或者元素为空的结构体
c = (*hchan)(mallocgc(hchanSize, nil, true))
//buf指向c的地址
c.buf = unsafe.Pointer(c)
case elem.kind&kindNoPointers != 0:
// 通过位运算知道 channel 中的元素不包含指针
// 这种情况下 gc 不会对 channel 中的元素进行 scan
c = (*hchan)(mallocgc(hchanSize+uintptr(size)*elem.size, nil, true))
c.buf = add(unsafe.Pointer(c), hchanSize)
default:
// 元素含有指针,两次分配空间的函数 new/mallocgc
c = new(hchan)
c.buf = mallocgc(uintptr(size)*elem.size, elem, true)
}
//其他字段赋值
c.elemsize = uint16(elem.size)
c.elemtype = elem
c.dataqsiz = uint(size)
return c
}
发送(chansend)
func chansend1(c *hchan, elem unsafe.Pointer) {
chansend(c, elem, true, getcallerpc())
}
func chansend(c *hchan, ep unsafe.Pointer, block bool, callerpc uintptr) bool {
if c == nil {
//为nil并且不阻塞直接返回
//向nil发送数据不panic,closed会
if !block {
return false
}
// 挂起当前 goroutine,会永远阻塞下去
gopark(nil, nil, "chan send (nil chan)", traceEvGoStop, 2)
throw("unreachable")
}
//这个语句为什么不上锁?
//因为是顺序执行,当判断c.dataqsiz == 0....时
//c.close==0一定成立
//即便在判断过程中c.close被其他goroutine修改
//也不影响逻辑
if !block && c.closed == 0 && ((c.dataqsiz == 0 && c.recvq.first == nil) ||
(c.dataqsiz > 0 && c.qcount == c.dataqsiz)) {
//非缓冲并且接收队列第一个sudog为nil
//有缓冲但是缓冲满了
return false
}
var t0 int64
if blockprofilerate > 0 {
t0 = cputicks()
}
lock(&c.lock)
// channel 已被关闭,panic
if c.closed != 0 {
unlock(&c.lock)
panic(plainError("send on closed channel"))
}
if sg := c.recvq.dequeue(); sg != nil {
passing the channel buffer (if any).
//如果接收队列有sudug
//说明缓冲为空或者非缓冲
//直接将元素拷贝给sudog
send(c, sg, ep, func() { unlock(&c.lock) }, 3)
return true
}
// 说明缓冲未满
if c.qcount < c.dataqsiz {
//指向发送下标
//发送下标位置接收发送过来的元素
qp := chanbuf(c, c.sendx)
// 将 goroutine 的数据拷贝到 buffer 中
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
}
// 在 channel 上阻塞,receiver 会唤醒
//获得当前的goroutine
gp := getg()
mysg := acquireSudog()
mysg.releasetime = 0
if t0 != 0 {
mysg.releasetime = -1
}
// 打包 sudog
mysg.elem = ep
mysg.waitlink = nil
mysg.g = gp
mysg.isSelect = false
mysg.c = c
gp.waiting = mysg
gp.param = nil
// 将当 goroutine 打包后的 sudog 入队到 channel 的 sendq 队列中
c.sendq.enqueue(mysg)
// 将这个发送 g 从 Grunning -> Gwaiting
// 进入休眠
goparkunlock(&c.lock, "chan send", traceEvGoBlockSend, 3)
// 唤醒后要执行的代码
if mysg != gp.waiting {
// 先判断当前是不是合法的休眠中
throw("G waiting list is corrupted")
}
gp.waiting = nil
//close时,也会将gp.param=nil
//发送完成后,gp.param会指向包装自己的sudug
if gp.param == nil {
//向关闭的c中发送数据
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
}
func send(c *hchan, sg *sudog, ep unsafe.Pointer, unlockf func(), skip int) {
// receiver 的 sudog 已经在对应区域分配过空间
// 直接拷贝数据
if sg.elem != nil {
sendDirect(c.elemtype, sg, ep)
sg.elem = nil
}
gp := sg.g
unlockf()
gp.param = unsafe.Pointer(sg)
if sg.releasetime != 0 {
sg.releasetime = cputicks()
}
// Gwaiting -> Grunnable
goready(gp, skip+1)
}
接收
//对应形式
func chanrecv1(c *hchan, elem unsafe.Pointer) {
chanrecv(c, elem, true)
}
//对应形式:i;ok:<-ch
func chanrecv2(c *hchan, elem unsafe.Pointer) (received bool) {
_, received = chanrecv(c, elem, true)
return
}
func chanrecv(c *hchan, ep unsafe.Pointer, block bool) (selected, received bool) {
// 如果在 nil channel 上进行 recv 操作,那么会永远阻塞
if c == nil {
if !block {
// 非阻塞的情况下
// 要直接返回
return
}
了
gopark(nil, nil, "chan receive (nil chan)", traceEvGoStop, 2)
throw("unreachable")
}
if !block && (c.dataqsiz == 0 && c.sendq.first == nil ||
c.dataqsiz > 0 && atomic.Loaduint(&c.qcount) == 0) &&
atomic.Load(&c.closed) == 0 {
// 非阻塞且没内容可收的情况下要直接返回
// 两个 bool 的零值就是 false,false
return
}
var t0 int64
if blockprofilerate > 0 {
t0 = cputicks()
}
lock(&c.lock)
// 当前 channel 中没有数据可读
// 直接返回 not selected
if c.closed != 0 && c.qcount == 0 {
unlock(&c.lock)
if ep != nil {
//清除接收变量的内存
typedmemclr(c.elemtype, ep)
}
return true, false
}
// sender 队列中有 sudog 在等待
// 直接从该 sudog 中获取数据拷贝到当前 g 即可
if sg := c.sendq.dequeue(); sg != nil {
recv(c, sg, ep, func() { unlock(&c.lock) }, 3)
return true, true
}
if c.qcount > 0 {
// Receive directly from queue
qp := chanbuf(c, c.recvx)
// 直接从 buffer 里拷贝数据
if ep != nil {
typedmemmove(c.elemtype, ep, qp)
}
typedmemclr(c.elemtype, qp)
// 接收索引 +1
c.recvx++
if c.recvx == c.dataqsiz {
c.recvx = 0
}
// buffer 元素计数 -1
c.qcount--
unlock(&c.lock)
return true, true
}
if !block {
unlock(&c.lock)
// 非阻塞时,且无数据可收
// 始终不选中,这是在 buffer 中没内容的时候
return false, false
}
gp := getg()
mysg := acquireSudog()
mysg.releasetime = 0
if t0 != 0 {
mysg.releasetime = -1
}
// 打包成 sudog
mysg.elem = ep
mysg.waitlink = nil
gp.waiting = mysg
mysg.g = gp
mysg.isSelect = false
mysg.c = c
gp.param = nil
// 进入 recvq 队列
c.recvq.enqueue(mysg)
// Grunning -> Gwaiting
goparkunlock(&c.lock, "chan receive", traceEvGoBlockRecv, 3)
// 被唤醒
if mysg != gp.waiting {
throw("G waiting list is corrupted")
}
gp.waiting = nil
if mysg.releasetime > 0 {
blockevent(mysg.releasetime-t0, 2)
}
closed := gp.param == nil
gp.param = nil
mysg.c = nil
releaseSudog(mysg)
// 如果 channel 未被关闭,那就是真的 recv 到数据了
return true, !closed
}
func recv(c *hchan, sg *sudog, ep unsafe.Pointer, unlockf func(), skip int) {
if c.dataqsiz == 0 {
if ep != nil {
recvDirect(c.elemtype, sg, ep)
}
} else {
qp := chanbuf(c, c.recvx)
if ep != nil {
typedmemmove(c.elemtype, ep, qp)
}
typedmemmove(c.elemtype, qp, sg.elem)
c.recvx++
if c.recvx == c.dataqsiz {
c.recvx = 0
}
c.sendx = c.recvx // 考虑到缓冲满的情况
}
sg.elem = nil
gp := sg.g
unlockf()
gp.param = unsafe.Pointer(sg)
if sg.releasetime != 0 {
sg.releasetime = cputicks()
}
// Gwaiting -> Grunnable
goready(gp, skip+1)
}
关闭
func closechan(c *hchan) {
// 关闭一个 nil channel 会直接 panic
if c == nil {
panic(plainError("close of nil channel"))
}
// 上锁,这个锁的粒度比较大,一直到释放完所有的 sudog 才解锁
lock(&c.lock)
// 在 close channel 时,如果 channel 已经关闭过了
// 直接触发 panic
if c.closed != 0 {
unlock(&c.lock)
panic(plainError("close of closed channel"))
}
c.closed = 1
var glist *g
// release all readers
for {
sg := c.recvq.dequeue()
// 弹出的 sudog 是 nil
// 说明读队列已经空了
if sg == nil {
break
}
// sg.elem unsafe.Pointer,指向 sudog 的数据元素
// 该元素可能在堆上分配,也可能在栈上
if sg.elem != nil {
// 释放对应的内存
typedmemclr(c.elemtype, sg.elem)
sg.elem = nil
}
if sg.releasetime != 0 {
sg.releasetime = cputicks()
}
// 将 goroutine 入 glist
// 为最后将全部 goroutine 都 ready 做准备
gp := sg.g
gp.param = nil
gp.schedlink.set(glist)
glist = gp
}
// 将所有挂在 channel 上的 writer 从 sendq 中弹出
// 该操作会使所有 writer panic
for {
sg := c.sendq.dequeue()
if sg == nil {
break
}
sg.elem = nil
if sg.releasetime != 0 {
sg.releasetime = cputicks()
}
// 将 goroutine 入 glist
// 为最后将全部 goroutine 都 ready 做准备
gp := sg.g
gp.param = nil
gp.schedlink.set(glist)
glist = gp
}
// 在释放所有挂在 channel 上的读或写 sudog 时
// 是一直在临界区的
unlock(&c.lock)
// Ready all Gs now that we've dropped the channel lock.
for glist != nil {
gp := glist
glist = glist.schedlink.ptr()
gp.schedlink = 0
// 使 g 的状态切换到 Grunnable
goready(gp, 3)
}
}
可以从关闭的通道中读取数据吗?
可以,只有当返回值是false才是无效的。
通道关闭原则?
单一sender,由sender关闭。多个sender,通过channel来做广播信号关闭。
channel使得goroutine泄露?
channel长期处于满或者空,使得goroutine一直处在Gwaiting的状态。
读写情况总结
唯一可能panic的就是向关闭的channel中写数据,向nil中读写会被一直阻塞。
注
本文部分引用github.com/cch123/golang-notes的内容。