Golang channel 的实现原理

本文深入探讨了Go语言中Channel的实现原理,包括其关键结构体hchan的设计、创建过程、元素的发送与接收机制以及如何关闭Channel等内容。

Channel 是golang语言自身提供的一种非常重要的语言特性, 它是实现任务执行队列、 协程间消息传递、高并发框架的基础。关于channel的用法的文章已经很多, 本文从channel源码的实现的角度, 讨论一下其实现原理。

关于channel放在: src/runtime/chan.go
channel的关键的结构体放在hchan里面, 它记录了channel实现的关键信息。
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

}

创建channel

用法: ch := make(chan TYPE, size int);
这里, type是channel里面传递的elem的类型;
size是channel缓存的大小:
如果为1, 代表非缓冲的channel, 表明channel里面最多有一个elem, 剩余的只能在channel外排队等待;
如果为0,
其实现代码如下:
func makechan(t *chantype, size int) *hchan {
// Hchan does not contain pointers interesting for GC when elements stored in buf do not contain pointers.
// buf points into the same allocation, elemtype is persistent.
// SudoG’s are referenced from their owning thread so they can’t be collected.
// TODO(dvyukov,rlh): Rethink when collector can move allocated objects.
var c *hchan
switch {
case size == 0 || elem.size == 0:
// Queue or element size is zero.
c = (*hchan)(mallocgc(hchanSize, nil, true))
// Race detector uses this location for synchronization.
c.buf = unsafe.Pointer(c)
case elem.kind&kindNoPointers != 0:
// Elements do not contain pointers.
// Allocate hchan and buf in one call.
c = (*hchan)(mallocgc(hchanSize+uintptr(size)*elem.size, nil, true))
c.buf = add(unsafe.Pointer(c), hchanSize)
default:
// Elements contain pointers.
c = new(hchan)
c.buf = mallocgc(uintptr(size)*elem.size, elem, true)
}

c.elemsize = uint16(elem.size)
c.elemtype = elem
c.dataqsiz = uint(size)

if debugChan {
    print("makechan: chan=", c, "; elemsize=", elem.size, "; elemalg=", elem.alg, "; dataqsiz=", size, "\n")
}
return c

}

写入channel elem

用法: ch <- elem
channel是阻塞时的管道, 从channel读取的时候,可能发生如下3种情况:

  • channel 已经关闭, 发生panic;
  • 从已经收到的队列内读取一个elem;
  • 从缓存队列内读取elem;
    lock(&c.lock)

    if c.closed != 0 {
        unlock(&c.lock)
        panic(plainError("send on closed channel"))
    }

    if sg := c.recvq.dequeue(); sg != nil {
        // Found a waiting receiver. We pass the value we want to send
        // directly to the receiver, bypassing the channel buffer (if any).
        send(c, sg, ep, func() { unlock(&c.lock) }, 3)
        return true
    }

    if c.qcount < c.dataqsiz {
        // Space is available in the channel buffer. Enqueue the element to send.
        qp := chanbuf(c, c.sendx)
        if raceenabled {
            raceacquire(qp)
            racerelease(qp)
        }
        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 elem读取elem

用法: elem, ok := <- ch
if !ok {
fmt.Println(“ch has been closed”)
}
3种可能返回值:

  • chan已经被关闭, OK为false, elem为该类型的空值;
  • 如果chan此时没有值存在, 该读取语句会一直等待直到有值;
  • 如果chan此时有值, 读取正确的值, ok为true;

代码实现:

// chanrecv receives on channel c and writes the received data to ep.
// ep may be nil, in which case received data is ignored.
// If block == false and no elements are available, returns (false, false).
// Otherwise, if c is closed, zeros *ep and returns (true, false).
// Otherwise, fills in *ep with an element and returns (true, true).
// A non-nil ep must point to the heap or the caller's stack.
func chanrecv(c *hchan, ep unsafe.Pointer, block bool) (selected, received bool) {
          // 正常的channel读取流程, 直接读取
     if sg := c.sendq.dequeue(); sg != nil {
        // Found a waiting sender. If buffer is size 0, receive value
        // directly from sender. Otherwise, receive from head of queue
        // and add sender's value to the tail of the queue (both map to
        // the same buffer slot because the queue is full).
        recv(c, sg, ep, func() { unlock(&c.lock) }, 3)
        return true, true
    }

 // 从缓存的elem队列读取一个elem  
  if c.qcount > 0 {
        // Receive directly from queue
        qp := chanbuf(c, c.recvx)
        if raceenabled {
            raceacquire(qp)
            racerelease(qp)
        }
        if ep != nil {
            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
    }   

  // channel没有可以读取的elem, 更新channel内部状态, 等待新的elem;   
}

关闭channel

用法: close(ch)
作用: 关闭channel, 并将channel内缓存的elem清除;

代码实现:

func closechan(c *hchan) {
    var glist *g

    // release all readers
    for {
        sg := c.recvq.dequeue()
        gp := sg.g
        gp.param = nil
        gp.schedlink.set(glist)
        glist = gp
    }

    // release all writers (they will panic)
    for {
        sg := c.sendq.dequeue()
        sg.elem = nil
        gp := sg.g
        gp.param = nil
        gp.schedlink.set(glist)
        glist = gp
    }
    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
        goready(gp, 3)
    }
}
### Golang Channel 的内部实现与工作原理 #### 创建 Channel 在 Go 中,`channel` 是通过 `make` 函数创建的。当调用 `make(chan Type)` 或者带有缓冲区大小参数的版本时,实际上是在堆上分配了一个新的通道对象,并返回指向该结构体的指针[^3]。 对于无缓冲区(同步)通道而言,发送方和接收方必须同时准备好才能完成一次通信;而对于有缓冲区的通道,则可以在一定范围内异步地执行发送操作而无需等待接收端准备就绪。这取决于初始化时指定的容量: ```go ci := make(chan int) // 同步整数型通道 cj := make(chan int, 0) // 明确声明为同步整数型通道 cs := make(chan *os.File, 100) // 带有100个文件指针缓存空间的通道 ``` #### 发送与接收机制 考虑如下场景,在向通道发送数据之前会先检查是否有写入器存在,如果没有则启动一个新的协程来处理后续的数据流并设置必要的缓冲区资源: ```go func (ch *Channel) Send(p Packet) { if c.noWriterYet() { go ch.writer() } ch.send <- p } ``` 这段代码展示了如何优雅地管理 writer 协程以及按需分配内存给待传输的消息队列[^1]。 具体来说,每当尝试往一个未被任何writer占用过的 channel 上发消息时,就会触发新goroutine 的创建用于持续监听来自其他地方对该特定 channel 的输入请求直到程序结束或显式关闭此连接为止。与此同时,如果当前不存在可用的空间容纳即将加入的新元素,则整个过程会被阻塞直至腾出足够的位置供其安置下来。 #### Goroutines 和 Channels 的交互方式 Go 提倡使用轻量级线程——即 goroutines 来构建高效的并发应用程序。这些 goroutines 可以借助于 channels 实现彼此间的通讯及协调活动。由于内置了锁机制和其他保护措施,因此开发者不必担心竞争条件等问题的发生,从而简化了多任务编程模型的设计复杂度[^2]。 总之,channels 不仅提供了简单易懂的方法来进行跨 goroutine 数据交换,而且还在背后做了大量优化工作以确保性能表现优异的同时保持 API 接口友好直观。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值