golang channel数据的接收发送流程

Channel底层数据结构说明

type hchan struct {
    qcount   uint           // 用于记录当前通道队列中已有的数据元素数量
    dataqsiz uint           // 指定了通道的循环队列(缓冲区)的大小。也就是通道最多能容纳的数据元素数量
    buf      unsafe.Pointer // 指向一个数组,该数组的大小由dataqsiz确定,用于存储通道中的数据元素
    elemsize uint16         // 通道中每个数据元素的大小(以字节为单位)
    closed   uint32         // 用于标记通道是否已经关闭。当该字段的值为1时,表示通道已经关闭:否则,通道处于正常的开放状态
    elemtype *_type         // 用于标识通道中所传输数据元素的类型,_type是Go语言内部用于表示类型信息的一种结构体
    sendx    uint           // 在有缓冲通道情况下,用于记录下一次发送数据时应该放入缓冲区的索引位置
    recvx    uint           // 在有缓冲通道情况下,用于记录下一次接收数据时应该从缓冲区中取出数据的索引位置
    recvq    waitq          // 一个等待队列,用于存放那些正在等待从该通道接收数据的协程(Goroutine)。等待读消息的goroutine队列
    sendq    waitq          // 一个等待队列,不过它用于存放那些正在等待向该通道发送数据的协程。等待写消息的goroutine队列
    lock mutex              // 一个互斥锁。它用于保护hchn结构体中的所有字段,确保在多协程环境下对通道的各种操作(如发送、接收、查询通道状态等)能够安全、有序地进行。在对通道进行任何操作时,都需要先获取这个锁,操作完成后再释放锁,以避免并发冲突导致的数据不一致或其他异常情况
}

Golang 有缓冲 Channel 的接收发送流程

在 Golang 中,有缓冲的 channel 是一种可以存储固定数量元素的通道。

创建有缓冲 Channel

// 创建一个容量为n的有缓冲channel
ch := make(chan Type, n)

其中 n 表示缓冲区大小,即可以存储的元素数量。

发送流程

当向有缓冲 channel 发送数据时,流程如下:

  1. 检查接收队列:首先检查是否有 goroutine 在等待接收数据

    • 如果有,直接将数据发送给第一个等待的接收者,然后唤醒该接收者
    • 如果没有,进入步骤 2
  2. 检查缓冲区:检查缓冲区是否已满

    • 如果缓冲区未满,将数据放入缓冲区,发送操作立即完成,发送 goroutine 继续执行
    • 如果缓冲区已满,进入步骤 3
  3. 阻塞等待:发送 goroutine 被阻塞,加入发送等待队列,直到出现以下情况之一:

    • 有接收者从 channel 接收数据,缓冲区有空间,数据被放入缓冲区
    • channel 被关闭,此时会触发 panic

接收流程

当从有缓冲 channel 接收数据时,流程如下:

  1. 检查发送队列:首先检查是否有 goroutine 在等待发送数据

    • 如果有,从第一个等待的发送者获取数据,然后唤醒该发送者
    • 如果没有,进入步骤 2
  2. 检查缓冲区:检查缓冲区是否为空

    • 如果缓冲区不为空,从缓冲区取出第一个数据,接收操作完成,接收 goroutine 继续执行
    • 如果缓冲区为空,进入步骤 3
  3. 检查 channel 状态:检查 channel 是否已关闭

    • 如果 channel 已关闭,返回该类型的零值,并将 ok 设为 false(如果使用 v, ok := <-ch 形式接收)
    • 如果 channel 未关闭,进入步骤 4
  4. 阻塞等待:接收 goroutine 被阻塞,加入接收等待队列,直到有数据可接收或 channel 被关闭

关闭 Channel 的影响

当 channel 被关闭时:

  • 所有等待发送的 goroutine 会 panic
  • 所有等待接收的 goroutine 会被唤醒,接收到对应类型的零值
  • 已经在缓冲区中的数据仍然可以被接收

示例代码

package main

import (
    "fmt"
    "time"
)

func main() {
    // 创建容量为3的缓冲channel
    ch := make(chan int, 3)
    
    // 发送数据,不会阻塞,因为缓冲区有空间
    ch <- 1
    ch <- 2
    ch <- 3
    fmt.Println("成功发送3个数据到缓冲区")
    
    // 启动一个goroutine在1秒后接收数据
    go func() {
        time.Sleep(1 * time.Second)
        fmt.Println("开始接收数据")
        fmt.Println(<-ch) // 输出1
        fmt.Println(<-ch) // 输出2
        fmt.Println("接收了2个数据")
    }()
    
    // 尝试发送第4个数据,此时缓冲区已满,会阻塞
    // 直到上面的goroutine接收了数据,缓冲区有空间
    ch <- 4
    fmt.Println("成功发送第4个数据")
    
    // 关闭channel
    close(ch)
    
    // 从已关闭但有数据的channel接收数据
    fmt.Println(<-ch) // 输出3
    fmt.Println(<-ch) // 输出4
    
    // 从已关闭且无数据的channel接收数据
    val, ok := <-ch
    fmt.Printf("值: %d, 是否成功: %t\n", val, ok) // 值: 0, 是否成功: false
    
    time.Sleep(2 * time.Second) // 等待goroutine完成
}

Golang 无缓冲 Channel 的接收发送流程

无缓冲的 channel(也称为同步 channel)是 Golang 中一种特殊的通道,它没有存储容量。无缓冲 channel 的一个重要特性是:发送和接收操作必须同时准备好才能完成,否则会阻塞。

创建无缓冲 Channel

// 创建一个无缓冲channel
ch := make(chan Type)
// 或者显式指定缓冲区大小为0
ch := make(chan Type, 0)

发送流程

当向无缓冲 channel 发送数据时,流程如下:

  1. 检查接收队列:首先检查是否有 goroutine 在等待接收数据

    • 如果有等待的接收者,直接将数据传递给第一个等待的接收者,唤醒该接收者,发送操作完成
    • 如果没有等待的接收者,进入步骤 2
  2. 阻塞等待:由于无缓冲 channel 没有存储空间,发送 goroutine 会被阻塞并加入发送等待队列,直到:

    • 有接收者准备好接收数据,此时数据直接传递给接收者
    • channel 被关闭,此时会触发 panic(向已关闭的 channel 发送数据会导致 panic)

接收流程

当从无缓冲 channel 接收数据时,流程如下:

  1. 检查发送队列:首先检查是否有 goroutine 在等待发送数据

    • 如果有等待的发送者,直接从第一个等待的发送者获取数据,唤醒该发送者,接收操作完成
    • 如果没有等待的发送者,进入步骤 2
  2. 检查 channel 状态:检查 channel 是否已关闭

    • 如果 channel 已关闭,返回该类型的零值,并将 ok 设为 false(如果使用 v, ok := <-ch 形式接收)
    • 如果 channel 未关闭,进入步骤 3
  3. 阻塞等待:接收 goroutine 被阻塞并加入接收等待队列,直到:

    • 有发送者发送数据
    • channel 被关闭

无缓冲 Channel 与有缓冲 Channel 的主要区别

  1. 同步性

    • 无缓冲 channel:发送和接收必须同时就绪,形成"同步"操作
    • 有缓冲 channel:发送和接收可以在不同时间进行,只要缓冲区未满/未空
  2. 阻塞条件

    • 无缓冲 channel:发送操作总是阻塞,直到有接收者准备好
    • 有缓冲 channel:只有当缓冲区满时,发送操作才会阻塞
  3. 用途

    • 无缓冲 channel:适用于需要精确控制 goroutine 同步的场景
    • 有缓冲 channel:适用于需要一定程度解耦发送和接收的场景

示例代码

package main

import (
    "fmt"
    "time"
)

func main() {
    // 创建无缓冲channel
    ch := make(chan int)
    
    // 启动接收者goroutine
    go func() {
        fmt.Println("接收者:准备接收数据")
        time.Sleep(2 * time.Second) // 模拟接收者延迟准备
        val := <-ch
        fmt.Printf("接收者:接收到数据 %d\n", val)
    }()
    
    // 主goroutine作为发送者
    fmt.Println("发送者:准备发送数据")
    fmt.Println("发送者:尝试发送数据,将被阻塞直到有接收者准备好")
    ch <- 42 // 这里会阻塞,直到接收者准备好
    fmt.Println("发送者:数据已发送")
    
    // 演示同步通信
    done := make(chan bool)
    
    go func() {
        fmt.Println("工作goroutine:开始执行任务")
        time.Sleep(1 * time.Second)
        fmt.Println("工作goroutine:任务完成")
        done <- true // 通知主goroutine任务已完成
    }()
    
    // 等待工作完成
    <-done // 阻塞直到接收到完成信号
    fmt.Println("主goroutine:收到完成信号,程序结束")
}

无缓冲 Channel 的内部实现要点

  1. 锁机制:channel 内部使用互斥锁保护其数据结构

  2. 等待队列:无缓冲 channel 维护两个等待队列

    • 发送等待队列:存储等待发送数据的 goroutine
    • 接收等待队列:存储等待接收数据的 goroutine
  3. 直接传递:无缓冲 channel 中的数据传递是直接从发送者到接收者,不经过中间存储

  4. 公平性:等待队列通常采用 FIFO(先进先出)策略,确保公平性

打 赏