作用

  1. Go 语言的 channel 是一种 goroutine 之间的通信方式,它可以用来传递数据,也可以用来同步 goroutine 的执行。
  2. chan 是 goroutine 之间的通信桥梁,可以安全地在多个 goroutine 中共享数据。
  3. 使用 chan 实现 goroutine 之间的协作与同步,可用于信号传递、任务完成通知等。
  4. select 配合 chan,可以同时监听多个 channel,处理任意一个可用 channel 的数据。

结构

type hchan struct {
qcount uint // 队列中的元素个数
dataqsiz uint // 环形队列的容量
buf unsafe.Pointer // 环形队列的指针
elemsize uint16 // 元素的大小
closed uint32 // 是否关闭 如果以关闭则不是0
timer *timer // 为此 channel 提供时间控制的计时器
elemtype *_type // 元素的类型
sendx uint // 发送索引,指示下一个发送操作的位置
recvx uint // 接收索引,指示下一个接收操作的位置
recvq waitq // 等待接收的等待队列
sendq waitq // 等待发送的等待队列 // 锁
lock mutex
}

waitq

type waitq struct {
first *sudog // 首指针
last *sudog // 尾指针
}

sudog

type sudog struct {
g *g // goroutine next *sudog // 指向下一个sudog,用于形成链表
prev *sudog // 指向上一个sudog,用于形成链表
elem unsafe.Pointer // 指向数据元素的指针(可能指向栈上的数据) acquiretime int64 // 获取资源的时间
releasetime int64 // 释放资源的时间
ticket uint32 // 票据号码,用于排序和公平性 isSelect bool // 标志是否在select操作中使用此sudog success bool // 通信是否成功(接收到值或因 channel 关闭被唤醒) waiters uint16 // 等待者数量,仅在列表头部有意义 parent *sudog // 指向父节点的指针,在二叉树结构中使用
waitlink *sudog // g的等待链表或semaRoot
waittail *sudog // semaRoot的尾部
c *hchan // 指向sudog所等待的 channel
}

创建

创建一个 channel:

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")
}
// 计算内存分配所需大小:`元素大小 * 数量`。
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:
// 队列大小为0 说明是无缓冲的channel 直接分配hchan
// 分配内存 hchanSize 是 hchan 结构体的大小
c = (*hchan)(mallocgc(hchanSize, nil, true))
c.buf = c.raceaddr()
case !elem.Pointers():
// 如果元素中不包含指针 则使用一个连续的内存块 结构体和 buf 是连续的
c = (*hchan)(mallocgc(hchanSize+mem, nil, true))
c.buf = add(unsafe.Pointer(c), hchanSize)
default:
// 如果元素中包含指针 则使用两个内存块
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
}
const hchanSize = unsafe.Sizeof(hchan{}) + uintptr(-int(unsafe.Sizeof(hchan{}))&(maxAlign-1))

func (c *hchan) raceaddr() unsafe.Pointer {
// 将对 channel 的读写操作视为发生在这个地址。
// 避免使用 `qcount` 或 `dataqsiz` 的地址,
// 因为内建函数 `len()` 和 `cap()` 会读取这些地址,
// 而我们不希望这些操作与例如 `close()` 之类的操作发生竞争。
return unsafe.Pointer(&c.buf)
}

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 {
if !block {
return false
}
// 如果 channel 为空 挂起当前 goroutine 并报错
gopark(nil, nil, waitReasonChanSendNilChan, traceBlockForever, 2)
throw("unreachable")
} // ...... // 检查非阻塞模式是否可以直接返回失败结果
if !block && c.closed == 0 && full(c) {
return false
} var t0 int64
if blockprofilerate > 0 {
t0 = cputicks()
} lock(&c.lock)
// 检查 channel 是否已经关闭
if c.closed != 0 {
unlock(&c.lock)
panic(plainError("send on closed channel"))
} if sg := c.recvq.dequeue(); sg != nil {
// 如果有等待接收的 Goroutine,直接将值发送给它,跳过缓冲区
send(c, sg, ep, func() { unlock(&c.lock) }, 3)
return true
} if c.qcount < c.dataqsiz {
// 如果通道缓冲区有空间,直接将值写入缓冲区
qp := chanbuf(c, c.sendx)
if raceenabled {
racenotify(c, c.sendx, nil)
}
typedmemmove(c.elemtype, qp, ep)
c.sendx++
if c.sendx == c.dataqsiz {
c.sendx = 0
}
c.qcount++
unlock(&c.lock)
return true
} if !block {
// 非阻塞模式且无法发送值,返回 false
unlock(&c.lock)
return false
} // 阻塞模式,当前 Goroutine 挂起等待接收者
gp := getg()
// 放入 acquireSudog
mysg := acquireSudog()
mysg.releasetime = 0
if t0 != 0 {
mysg.releasetime = -1
} 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) gp.parkingOnChan.Store(true)
gopark(chanparkcommit, unsafe.Pointer(&c.lock), waitReasonChanSend, traceBlockChanSend, 2)
// 确保发送值在接收者拷贝之前不会被释放
KeepAlive(ep) // 唤醒后,检查状态
if mysg != gp.waiting {
throw("G waiting list is corrupted")
}
gp.waiting = nil
gp.activeStackChans = false
closed := !mysg.success
gp.param = nil
if mysg.releasetime > 0 {
blockevent(mysg.releasetime-t0, 2)
}
mysg.c = nil
// 回收 sudog
releaseSudog(mysg)
if closed {
if c.closed == 0 {
throw("chansend: spurious wakeup")
}
panic(plainError("send on closed channel"))
}
return true
}

所以阻塞写这个主要有三种模式:

  1. 如果有等待接收的 Goroutine (c.recvq 里面有值),说明 buf 要么满了 要么就没有,直接将值发送给它,跳过缓冲区
  2. 如果通道缓冲区有空间,直接将值写入缓冲区
  3. 如果缓冲区没有空间,且是阻塞模式,当前 Goroutine 挂起等待接收者

send

func send(c *hchan, sg *sudog, ep unsafe.Pointer, unlockf func(), skip int) {
// ...... // 如果接收者有一个有效的元素指针,则将发送者的数据直接拷贝给接收者
if sg.elem != nil {
sendDirect(c.elemtype, sg, ep)
sg.elem = nil
}
gp := sg.g
unlockf()
gp.param = unsafe.Pointer(sg)
sg.success = true
if sg.releasetime != 0 {
sg.releasetime = cputicks()
} // 唤醒接收者 Goroutine
goready(gp, skip+1)
}
func sendDirect(t *_type, sg *sudog, src unsafe.Pointer) {
// 内存拷贝
dst := sg.elem
typeBitsBulkBarrier(t, uintptr(dst), uintptr(src), t.Size_)
memmove(dst, src, t.Size_)
}

大致的逻辑为,取出 goroutine 然后把接受的值 COPY 到接受者的内存中,然后唤醒接受者 goroutine。

接受的内存可能是堆也可能是栈,堆还好说,如果是栈,就是在一个栈内直接操作其他的栈了,按理来说,这是不安全的。但是,这是 runtime, 我们已经把 goroutine GoPark 了,保证了它不会执行,所以这里是安全的。当然我们自己写代码时,肯定是不能这么做的。

chanbuf && typedmemmove

func chanbuf(c *hchan, i uint) unsafe.Pointer {
// 在 buf 上加上 i * elemsize 的偏移量
return add(c.buf, uintptr(i)*uintptr(c.elemsize))
} func typedmemmove(typ *abi.Type, dst, src unsafe.Pointer) {
if dst == src {
return
}
if writeBarrier.enabled && typ.Pointers() {
// 如果写屏障启用且类型包含指针,则需要处理写屏障。
bulkBarrierPreWrite(uintptr(dst), uintptr(src), typ.PtrBytes, typ)
}
// 执行内存拷贝
memmove(dst, src, typ.Size_)
if goexperiment.CgoCheck2 {
cgoCheckMemmove2(typ, dst, src, 0, typ.Size_)
}
}

acquireSudog & releaseSudog

func acquireSudog() *sudog {
mp := acquirem()
pp := mp.p.ptr() // 如果 sudog 缓存为空,需要补充缓存
if len(pp.sudogcache) == 0 {
lock(&sched.sudoglock)
for len(pp.sudogcache) < cap(pp.sudogcache)/2 && sched.sudogcache != nil {
s := sched.sudogcache
sched.sudogcache = s.next
s.next = nil
pp.sudogcache = append(pp.sudogcache, s)
}
unlock(&sched.sudoglock)
if len(pp.sudogcache) == 0 {
pp.sudogcache = append(pp.sudogcache, new(sudog))
}
} // 从 P 的缓存中取出一个 sudog
n := len(pp.sudogcache)
s := pp.sudogcache[n-1]
pp.sudogcache[n-1] = nil
pp.sudogcache = pp.sudogcache[:n-1]
if s.elem != nil {
throw("acquireSudog: found s.elem != nil in cache")
}
releasem(mp)
return s
} func releaseSudog(s *sudog) {
// ......
gp := getg() mp := acquirem()
pp := mp.p.ptr()
if len(pp.sudogcache) == cap(pp.sudogcache) {
// 如果本地缓存已满,将部分 sudog 转移到全局缓存
var first, last *sudog
for len(pp.sudogcache) > cap(pp.sudogcache)/2 {
n := len(pp.sudogcache)
p := pp.sudogcache[n-1]
pp.sudogcache[n-1] = nil
pp.sudogcache = pp.sudogcache[:n-1]
if first == nil {
first = p
} else {
last.next = p
}
last = p
}
lock(&sched.sudoglock)
last.next = sched.sudogcache
sched.sudogcache = first
unlock(&sched.sudoglock)
}
// 将 sudog 放回本地缓存
pp.sudogcache = append(pp.sudogcache, s)
releasem(mp)
}

func chanrecv1(c *hchan, elem unsafe.Pointer) {
chanrecv(c, elem, true)
} // 带 ok 时 比如 `v, 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) {
// ...... // 非阻塞模式下检查失败条件
if !block && empty(c) {
if atomic.Load(&c.closed) == 0 {
// 已经没关闭 直接返回 因为这是非阻塞模式而且 buf 为空的情况
return
}
if empty(c) {
if raceenabled {
raceacquire(c.raceaddr())
}
if ep != nil {
typedmemclr(c.elemtype, ep)
}
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 {
// 把数据清零 因为通道已经关闭了
typedmemclr(c.elemtype, ep)
}
return true, false
}
} else {
// 通道未关闭,检查是否有等待发送的 Goroutine
if sg := c.sendq.dequeue(); sg != nil {
// 直接从发送队列中取出值
recv(c, sg, ep, func() { unlock(&c.lock) }, 3)
return true, true
}
} // 如果缓冲区中有数据,从缓冲区接收
if c.qcount > 0 {
qp := chanbuf(c, c.recvx)
if raceenabled {
racenotify(c, c.recvx, nil)
}
if ep != nil {
// 直接 COPY 内存
typedmemmove(c.elemtype, ep, qp)
}
typedmemclr(c.elemtype, qp)
c.recvx++
if c.recvx == c.dataqsiz {
c.recvx = 0
}
c.qcount--
unlock(&c.lock)
return true, true
} // 如果是非阻塞接收,直接返回
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)
if c.timer != nil {
blockTimerChan(c)
} gp.parkingOnChan.Store(true)
gopark(chanparkcommit, unsafe.Pointer(&c.lock), waitReasonChanReceive, traceBlockChanRecv, 2) // someone woke us up
if mysg != gp.waiting {
throw("G waiting list is corrupted")
}
if c.timer != nil {
unblockTimerChan(c)
}
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
}

所以读数据(阻塞读)的逻辑为:

  1. 检查 channel 是否已经关闭 如果关闭了 而且没有数据了 直接返回
  2. 如果有等待发送的 Goroutine (c.sendq 里面有值),如果无缓冲chan 直接从goroutine中取值 负责从 buf 取出值 并把数据加入末尾
  3. 如果缓冲区中有数据,从缓冲区接收
  4. 如果缓冲区没有数据了 挂起 goroutine 并加入 recvq 等待接收者

recv

func recv(c *hchan, sg *sudog, ep unsafe.Pointer, unlockf func(), skip int) {
if c.dataqsiz == 0 { if ep != nil {
// 如果是无缓冲通道,直接 Copy 数据
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 // c.sendx = (c.sendx+1) % c.dataqsiz
}
sg.elem = nil
gp := sg.g
unlockf()
gp.param = unsafe.Pointer(sg)
sg.success = true
if sg.releasetime != 0 {
sg.releasetime = cputicks()
}
goready(gp, skip+1)
}

关闭

func closechan(c *hchan) {
// 空值检查
if c == nil {
panic(plainError("close of nil channel"))
} lock(&c.lock)
// 如果已经关闭了 报错 不能关闭已经关闭的 channel
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 // 创建一个 G 列表,用于保存需要唤醒的 Goroutine
var glist gList // 释放所有的读方
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 = unsafe.Pointer(sg)
sg.success = false
if raceenabled {
raceacquireg(gp, c.raceaddr())
}
glist.push(gp)
} // 释放所有的写方 会 panic 因为向已经关闭的 channel 写数据是不允许的
for {
sg := c.sendq.dequeue()
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) // 唤醒所有的 Goroutine
for !glist.empty() {
gp := glist.pop()
gp.schedlink = 0
goready(gp, 3)
}
}

非阻塞读写

非阻塞的方式一般用在 select 中。

func selectnbsend(c *hchan, elem unsafe.Pointer) (selected bool) {
return chansend(c, elem, false, getcallerpc())
} func selectnbrecv(elem unsafe.Pointer, c *hchan) (selected, received bool) {
return chanrecv(c, elem, false)
}
  • 在阻塞下,需要当前 goroutine 挂起时,非阻塞则不需要,直接返回 flase。
  • 如果能直接读数据,则返回 true。

select

func walkSelectCases(cases []*ir.CommClause) []ir.Node {
// ......
switch n.Op() {
default:
base.Fatalf("select %v", n.Op()) case ir.OSEND:
// if selectnbsend(c, v) { body } else { default body }
n := n.(*ir.SendStmt)
ch := n.Chan
cond = mkcall1(chanfn("selectnbsend", 2, ch.Type()), types.Types[types.TBOOL], r.PtrInit(), ch, n.Value) case ir.OSELRECV2:
n := n.(*ir.AssignListStmt)
recv := n.Rhs[0].(*ir.UnaryExpr)
ch := recv.X
elem := n.Lhs[0]
if ir.IsBlank(elem) {
elem = typecheck.NodNil()
}
cond = typecheck.TempAt(base.Pos, ir.CurFunc, types.Types[types.TBOOL])
fn := chanfn("selectnbrecv", 2, ch.Type())
call := mkcall1(fn, fn.Type().ResultsTuple(), r.PtrInit(), elem, ch)
as := ir.NewAssignListStmt(r.Pos(), ir.OAS2, []ir.Node{cond, n.Lhs[1]}, []ir.Node{call})
r.PtrInit().Append(typecheck.Stmt(as))
} // ......
}
func selectnbsend(c *hchan, elem unsafe.Pointer) (selected bool) {
return chansend(c, elem, false, getcallerpc())
} func selectnbrecv(elem unsafe.Pointer, c *hchan) (selected, received bool) {
return chanrecv(c, elem, false)
}

改写后就是调用 selectnbsend 非阻塞的从 channel 发送数据,如果成功则返回 true,否则返回 false。失败了就从下个 case 继续执行。

Go channel 原理的更多相关文章

  1. go channel原理及使用场景

    转载自:go channel原理及使用场景 源码解析 type hchan struct { qcount uint // Channel 中的元素个数 dataqsiz uint // Channe ...

  2. golang channel原理

    channel介绍 channel一个类型管道,通过它可以在goroutine之间发送和接收消息.它是Golang在语言层面提供的goroutine间的通信方式. 众所周知,Go依赖于称为CSP(Co ...

  3. go语言之行--golang核武器goroutine调度原理、channel详解

    一.goroutine简介 goroutine是go语言中最为NB的设计,也是其魅力所在,goroutine的本质是协程,是实现并行计算的核心.goroutine使用方式非常的简单,只需使用go关键字 ...

  4. [GO语言的并发之道] Goroutine调度原理&Channel详解

    并发(并行),一直以来都是一个编程语言里的核心主题之一,也是被开发者关注最多的话题:Go语言作为一个出道以来就自带 『高并发』光环的富二代编程语言,它的并发(并行)编程肯定是值得开发者去探究的,而Go ...

  5. go--->共享内存和通信两种并发模式原理探究

    共享内存和通信两种并发模式原理探究 并发理解 人类发明计算机编程的本质目的是为了什么呢?毫无疑问是为了解决人类社会中的各种负责业务场景问题.ok,有了这个出发点,那么想象一下,比如你既可以一心一意只做 ...

  6. NIO详解

    目录 NIO 前言 IO与NIO的区别 Buffer(缓冲区) Channel(通道) Charset(字符集) NIO遍历文件 NIO 前言 NIO即New IO,这个库是在JDK1.4中才引入的. ...

  7. 源码(chan,map,GMP,mutex,context)

    目录 1.chan原理 1.1 chan底层数据结构 1.2 创建channel原理 1.3 写入channel原理 1.4 读channel原理 1.5 关闭channel原理 1.6 总结 2.m ...

  8. golang channel的使用以及调度原理

    golang channel的使用以及调度原理 为了并发的goroutines之间的通讯,golang使用了管道channel. 可以通过一个goroutines向channel发送数据,然后从另一个 ...

  9. 图解Go的channel底层原理

    废话不多说,直奔主题. channel的整体结构图 简单说明: buf是有缓冲的channel所特有的结构,用来存储缓存数据.是个循环链表 sendx和recvx用于记录buf这个循环链表中的发送或者 ...

  10. 大神是如何学习 Go 语言之 Channel 实现原理精要

    转自: https://mp.weixin.qq.com/s/ElzD2dXWeldYkJmVVY6Djw 作者Draveness Go 语言中的管道 Channel 是一个非常有趣的数据结构,作为语 ...

随机推荐

  1. 动态去读 dll 文件

    // 反射动态读取 dll // Assembly assembly = Assembly.LoadFile(); 路径 // Assembly assembly = Assembly.LoadFro ...

  2. FirewallD is not running 原因与解决方法

    解决方法关于linux系统防火墙: centos5.centos6.redhat6系统自带的是iptables防火墙.centos7.redhat7自带firewall防火墙.ubuntu系统使用的是 ...

  3. 使用 GPU-Operator 与 KubeSphere 简化深度学习训练与 GPU 监控

    本文将从 GPU-Operator 概念介绍.安装部署.深度训练测试应用部署,以及在 KubeSphere 使用自定义监控面板对接 GPU 监控,从原理到实践,逐步浅析介绍与实践 GPU-Operat ...

  4. Spring实现MySQL事务操作

    一.创建数据库表 表名:account 字段:(`id`,`username`,`money`) 二.dao.service层创建业务接口.类 1 public interface UserDao { ...

  5. keycloak~token配置相关说明

    会话有效期 在 Keycloak 中,"SSO Session Idle" 和 "SSO Session Max" 是用于配置单点登录(SSO)会话的两个参数. ...

  6. 如何优雅地将AI人工智能在线客服嵌入企业网站

    随着人工智能(AI)技术的飞速发展,越来越多的企业意识到,将AI客服嵌入企业网站是提升客户体验.提高工作效率的重要手段.相比于传统的人工客服,AI客服可以24/7全天候服务,不仅能有效处理大部分用户问 ...

  7. 每日学学Java开发规范,编程规约(附阿里巴巴Java开发手册(终极版))

    前言 每次去不同的公司,码不同的代码,适应不同的规范,经常被老大教育规范问题,我都有点走火入魔的感觉,还是要去看看阿里巴巴Java开发规范,从中熟悉一下,纠正自己,码出高效,码出质量. 想细看的可以去 ...

  8. .NET 全功能流媒体管理控制接口平台

    前言 视频会议.在线教育.直播娱乐还是远程监控,流媒体平台的性能和稳定性直接影响着用户体验. 给大家推荐一个基于 C# 开发的全功能流媒体管理控制接口平台. 项目介绍 AKStream是一个基于 C# ...

  9. 模算术 modular arithmetic

    https://en.wikipedia.org/wiki/Modular_arithmetic#Integers_modulo_n 模算术: 整数达到特定值时会' 折返 ' 回来-- 模数 modu ...

  10. K8s之运行时containerd安装和使用

    一.containerd 1. 前生今世 很久以前,Docker 强势崛起,以"镜像"这个大招席卷全球,对其他容器技术进行致命的降维打击,使其毫无招架之力,就连 Google 也不 ...