深入剖析 Golang Channel 底层实现

目录

深入剖析 Golang Channel 底层实现

一、Channel 的应用场景

(一)协程间通讯

(二)组合多个逻辑

二、Channel 中的重要数据结构

(一)hchan结构体

(二)sudog结构体

三、Channel 如何保证线程安全

四、有缓冲区和无缓冲区 Channel 的区别

(一)表面区别

(二)底层实现区别

五、Channel 的同步读写、异步读写和阻塞读写

(一)同步读写

(二)异步读写

(三)阻塞读写

六、Channel 关闭的广播通知实现


在 Golang 编程中,Channel 是一个极为重要的特性,它为协程(Goroutine)之间的通信和同步提供了强大的支持。然而,你是否曾好奇过 Channel 在底层究竟是如何运作的呢?本文将深入探讨 Golang Channel 的底层实现原理,带你一窥其神秘面纱之后的奥秘。

一、Channel 的应用场景

(一)协程间通讯

在 Golang 中,倡导 “不要通过共享内存进行通讯,建议通过通讯来共享内存” 的理念,而 Channel 正是实现这一理念的关键工具。它允许协程之间安全、高效地传递数据,避免了传统共享内存带来的并发问题,如数据竞争等。

(二)组合多个逻辑

Channel 常与select语句配合使用,实现复杂的组合逻辑。通过select,我们可以同时监听多个 Channel 的读写操作,根据不同的事件触发相应的逻辑,从而构建出灵活且高效的并发程序。

二、Channel 中的重要数据结构

(一)hchan结构体

在 Golang 的运行时(runtime)包中,channel文件定义了hchan结构体,它是 Channel 的核心数据结构。

  1. qcount:表示队列中数据的总数,这里的队列指的是缓冲区队列(如果 Channel 有缓冲区的话)。
  2. dataqsiz:环形队列的大小,即缓冲区的容量。
  3. buffer:指向缓冲区的地址。实际上,缓冲区在内存中是连续分配的空间,虽然使用方式类似环形队列(通过记录发送和接收索引来实现循环读写),但并非真正意义上的环形队列结构。
  4. elemsizeelemtype:分别表示元素的大小和类型。
  5. close:用于标记 Channel 的关闭状态。
  6. sendxrecvx:已发送元素和已接收元素在环形队列中的索引位置。
  7. recvqsendq:这两个字段分别是用于保存接收者和发送者的双向链表队列。链表中的每个元素都是一个sudog结构体,用于包装等待的协程。
  8. lock:用于保护 Channel 中的字段以及阻塞在 Channel 上的协程相关字段,确保线程安全。

(二)sudog结构体

sudog结构体用于包装等待的协程,它包含了指向协程的指针以及与数据传递相关的信息。其中,elem字段指定了协程发送或接收数据的存储位置。

三、Channel 如何保证线程安全

Channel 通过hchan结构体中的lock字段来保证线程安全。在对 Channel 进行读写以及关闭操作时,都会先获取锁,操作完成后再释放锁。这样可以确保同一时间只有一个协程能够访问 Channel,避免了并发冲突。

例如,在chan_send函数中,发送数据前会先加锁:

func chan_send(c *hchan, ep unsafe.Pointer, block bool, callerpc uintptr) bool {
    // 加锁操作
    lock(&c.lock)
    //... 其他操作
    unlock(&c.lock)
    return true
}

chan_recv函数接收数据时也有类似的加锁逻辑:

func chan_recv(c *hchan, ep unsafe.Pointer, block bool) (selected, received bool) {
    // 加锁操作
    lock(&c.lock)
    //... 其他操作
    unlock(&c.lock)
    return true, true
}

closechan函数关闭 Channel 时同样先加锁:

func closechan(c *hchan) {
    // 加锁操作
    lock(&c.lock)
    //... 其他操作
    unlock(&c.lock)
}

四、有缓冲区和无缓冲区 Channel 的区别

(一)表面区别

  1. 有缓冲区的 Channel 可以预先存储数据,即使没有接收方,也能在一定程度上写入数据(取决于缓冲区大小)。
  2. 无缓冲区的 Channel 在写入数据时必须有接收方,否则写入方会阻塞。

(二)底层实现区别

  1. 无缓冲区的 Channel
    • 底层数据结构中,与缓冲区相关的字段(如qcountdataqsizbuffersendxrecvx)不会被使用,主要依赖recvqsendqlock
    • 当接收方先执行receive操作且 Channel 为空时,接收协程会被包装成sudog并添加到recvq中,然后挂起等待。当有发送方执行send操作时,会从recvq中取出接收协程,直接将数据复制给接收方,实现同步发送。反之,如果发送方先执行send操作且没有接收方,发送协程会被包装成sudog添加到sendq中并挂起,直到有接收方执行receive操作。
  2. 有缓冲区的 Channel
    • 底层数据结构中的所有字段在缓冲区操作中都会被用到。
    • 发送数据时,如果recvq中有等待的接收者,直接将数据发送给接收者并唤醒接收协程;如果没有接收者且缓冲区未满,则将数据发送到缓冲区,并更新sendxqcount。当缓冲区满时,发送方会阻塞,等待接收方从缓冲区读取数据后腾出空间。
    • 接收数据时,如果sendq中有等待的发送者,直接从发送者获取数据;如果缓冲区有数据,则从缓冲区读取数据并更新recvxqcount。当缓冲区为空且没有发送者时,接收方会阻塞,等待发送方发送数据。

五、Channel 的同步读写、异步读写和阻塞读写

(一)同步读写

  1. 同步读:当sendq中有数据时,接收操作会从sendq中获取一个sudog,将数据复制给当前协程,立即得到结果。例如:

data := <-ch // 从Channel中同步读取数据

  1. 同步写:当recvq中有等待的接收者时,发送操作会将数据直接复制给接收者。例如:
ch <- data // 向Channel中同步写入数据

(二)异步读写

  1. 异步读:数据从缓冲区读取,即使没有直接的发送方,只要缓冲区中有数据,接收操作就能获取到。例如:
// 假设ch是有缓冲区的Channel
// 发送方在其他地方向ch发送了数据
data := <-ch // 可以异步读取缓冲区中的数据

  1. 异步写:发送数据到缓冲区,不等待接收方立即处理。例如:
// 假设ch是有缓冲区的Channel
ch <- data // 将数据异步写入缓冲区

(三)阻塞读写

  1. 阻塞写:当 Channel 没有缓冲区或者缓冲区已满时,发送操作会阻塞,直到有接收方从 Channel 读取数据腾出空间。例如:
// 假设ch是无缓冲区或者缓冲区已满的Channel
ch <- data // 发送操作会阻塞

  1. 阻塞读:当 Channel 为空且没有发送方时,接收操作会阻塞,直到有发送方发送数据。例如:
data := <-ch // 接收操作会阻塞

六、Channel 关闭的广播通知实现

Channel 关闭时,会通过一系列操作实现广播通知。

  1. 首先,将close状态标记为已关闭,并获取锁。
  2. 接着,遍历接收者队列recvq,将每个接收者的elem字段置空,并将其添加到一个列表中。
  3. 然后,遍历发送者队列sendq,将每个发送者的相关数据置空。
  4. 完成上述操作后,释放锁。
  5. 之后,当其他协程尝试对已关闭的 Channel 进行读或写操作时,读操作会接收到零值,写操作会报错。
  6. 最后,通过循环激活所有在等待队列中挂起的协程,接收协程在被激活后执行receive操作时会接收到零值,从而得知 Channel 已关闭。

以下是简化后的closechan函数逻辑(实际源码更为复杂):

func closechan(c *hchan) {
    // 加锁
    lock(&c.lock)
    c.close = true

    // 遍历接收者队列
    for sg := range c.recvq {
        sg.elem = nil
        // 将接收者添加到列表(这里省略具体实现)
    }

    // 遍历发送者队列
    for sg := range c.sendq {
        sg.elem = nil
    }

    // 释放锁
    unlock(&c.lock)

    // 激活等待队列中的协程(这里省略具体实现)
}

通过以上对 Golang Channel 底层实现的深入剖析,我们可以更好地理解 Channel 的工作原理,从而在编写并发程序时更加得心应手,充分发挥 Golang 在并发编程方面的优势。希望本文能为你在探索 Golang 底层奥秘的道路上提供有益的帮助。如果你对 Golang 的其他特性或底层实现感兴趣,欢迎继续关注我的博客,我将持续为你带来更多深入的技术解读。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值