LiSheng's blog LiSheng's blog
首页
笔记
个人简历
随笔集
GitHub (opens new window)
首页
笔记
个人简历
随笔集
GitHub (opens new window)
  • golang

    • 并发编程

      • GMP并发模型
      • 锁相关
      • groutine并发相关
      • go如何实现原子操作
    • 内存管理

    • 数组和切片的区别
    • new和make
    • go defer
    • context
    • channel
      • go map
      • interface
      • 对象系统
      • rune 类型
      • 字符串拼接的几种方式
    • cplus

    • leetcode

    • 存储技术

    • 分布式系统

    • 计算机网络

    • Linux操作系统

    • Redis

    • 其他

    • 笔记
    • golang
    lisheng
    2024-09-10
    目录

    channel

    1、channel 是否线程安全?锁用在什么地方? 2、go channel 的底层实现原理 (数据结构) 3、nil、关闭的 channel、有数据的 channel,再进行读、写、关闭会怎么样?(各类变种题型) 4、向 channel 发送数据和从 channel 读数据的流程是什么样的?

    # 1. Channel 的基本概念

    • 问题: 什么是 Go 语言中的 Channel?它有什么特点?
    • 回答要点:
      • Channel 是一种类型安全的通信机制,用于在多个 Goroutine 之间传递消息。
      • Channel 具有类型,即只能发送和接收特定类型的数据。
      • Channel 本质上是一个 FIFO 队列,具有阻塞特性。
      • Go 语言中的 Channel 是并发安全的。

    # 2. 如何创建和使用 Channel?

    • 问题: 请写出创建和使用 Channel 的代码示例,并解释基本的 Channel 操作。
    • 回答要点:
      ch := make(chan int) // 创建一个无缓冲的 Channel
      
      go func() {
          ch <- 42 // 发送数据到 Channel
      }()
      
      value := <-ch // 从 Channel 接收数据
      fmt.Println(value)
      
      1
      2
      3
      4
      5
      6
      7
      8
      • 创建:使用 make 函数创建 Channel。
      • 发送数据:使用 <- 运算符将数据发送到 Channel。
      • 接收数据:使用 <- 运算符从 Channel 接收数据。

    # 3. 无缓冲 Channel 与有缓冲 Channel 的区别

    • 问题: 请解释无缓冲 Channel 和有缓冲 Channel 的区别,以及它们的适用场景。
    • 回答要点:
      • 无缓冲 Channel:
        • 发送和接收操作是同步的,即发送者必须等待接收者接收数据,反之亦然。
        • 适用于精确同步的场景,如事件通知或工作交接。
      • 有缓冲 Channel:
        • 发送操作是非阻塞的,直到缓冲区满为止;接收操作是非阻塞的,直到缓冲区为空为止。
        • 适用于允许一定程度异步操作的场景,如任务队列、生产者-消费者模型。
      • 示例:
        ch := make(chan int, 3) // 创建一个缓冲区大小为 3 的 Channel
        
        1

    # 4. Channel 的阻塞与非阻塞操作

    • 问题: 什么是 Channel 的阻塞操作?如何实现 Channel 的非阻塞操作?
    • 回答要点:
      • 阻塞操作: 如果 Channel 已满(对于有缓冲 Channel)或没有接收者(对于无缓冲 Channel),发送操作会阻塞;如果 Channel 为空,接收操作会阻塞。
      • 非阻塞操作:
        • 通过 select 语句实现:
          select {
          case ch <- value:
              fmt.Println("Sent:", value)
          default:
              fmt.Println("Channel is full, cannot send")
          }
          
          1
          2
          3
          4
          5
          6
        • 通过 ok 模式判断:
          value, ok := <-ch
          if ok {
              fmt.Println("Received:", value)
          } else {
              fmt.Println("Channel is closed")
          }
          
          1
          2
          3
          4
          5
          6

    # 5. Channel 关闭与判断

    • 问题: 如何关闭一个 Channel?如何判断一个 Channel 是否已经关闭?
    • 回答要点:
      • 使用 close 函数关闭一个 Channel:
        close(ch)
        
        1
      • 判断 Channel 是否关闭:
        value, ok := <-ch
        if !ok {
            fmt.Println("Channel is closed")
        }
        
        1
        2
        3
        4
      • 关闭后的 Channel 仍然可以接收数据,但不能再发送数据,否则会引发 panic。

    # 6. Channel 的遍历操作

    • 问题: 如何遍历一个已经关闭的 Channel 中的所有数据?
    • 回答要点:
      ch := make(chan int, 5)
      go func() {
          for i := 0; i < 5; i++ {
              ch <- i
          }
          close(ch)
      }()
      
      for value := range ch {
          fmt.Println(value)
      }
      
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      • 使用 range 关键字遍历 Channel,遍历会在 Channel 关闭且数据接收完毕后自动结束。

    # 7. select 语句的用法

    • 问题: 什么是 select 语句?如何使用它来处理多个 Channel 操作?
    • 回答要点:
      • select 语句用于监听多个 Channel 的操作,当某个 Channel 准备好时,会执行对应的 case 语句。
      • select 可以实现 Channel 的多路复用、超时控制、非阻塞操作等。
      • 示例:
        select {
        case value := <-ch1:
            fmt.Println("Received from ch1:", value)
        case ch2 <- 42:
            fmt.Println("Sent to ch2")
        case <-time.After(time.Second):
            fmt.Println("Timeout")
        default:
            fmt.Println("No operation ready")
        }
        
        1
        2
        3
        4
        5
        6
        7
        8
        9
        10

    # 8. Channel 的超时操作

    • 问题: 如何实现 Channel 操作的超时控制?
    • 回答要点:
      • 使用 time.After 函数与 select 语句结合,实现超时控制:
        select {
        case value := <-ch:
            fmt.Println("Received:", value)
        case <-time.After(time.Second):
            fmt.Println("Timeout")
        }
        
        1
        2
        3
        4
        5
        6

    # 9. nil Channel 的行为

    • 问题: 向一个 nil Channel 发送或接收数据会发生什么?
    • 回答要点:
      • nil Channel 会导致永久阻塞,不会执行任何操作。
      • 典型场景:通过设置 Channel 为 nil 来动态控制 select 语句中 case 的激活状态。

    在 Go 中,尝试向一个 nil channel 发送或接收数据会导致程序阻塞,这是因为 nil channel 没有任何接收者和发送者。

    如果你尝试向一个 nil channel 发送数据,例如:

    var ch chan int
    ch <- 1  // 向 nil channel 发送数据
    
    1
    2

    这段代码将导致程序无休止地阻塞,因为没有任何 Goroutine 在准备接收数据,且 nil channel 不会处理任何传输。

    同样,尝试从一个 nil channel 接收数据也会导致程序阻塞:

    var ch chan int
    data := <-ch  // 从 nil channel 接收数据
    
    1
    2

    在这个例子中,程序也会无休止地阻塞,因为没有任何数据可供接收,且 nil channel 不能接收任何数据。

    一般情况下,在使用 channel 前,要确保 channel 已经被正确初始化,例如:

    ch := make(chan int)
    // 然后使用 ch 进行发送和接收
    
    1
    2

    对一个nil chan写入数据写入会阻塞 对一个nil chan读取数据会阻塞。 对一个已关闭的chan写入数据写入会panic 对一个已关闭的chan读取数据写入会立即返回默认值。

    # 10. Channel 的多生产者与多消费者模式

    • 问题: 如何实现一个多生产者-多消费者模型?有什么注意事项?
    • 回答要点:
      • 可以直接使用 Channel 实现多生产者-多消费者模型,因为 Go 的 Channel 是并发安全的。
      • 关键在于确保生产者和消费者对 Channel 的关闭有明确的控制,避免出现数据丢失或死锁。
      • 示例:
        ch := make(chan int, 10)
        var wg sync.WaitGroup
        
        // 生产者
        for i := 0; i < 3; i++ {
            wg.Add(1)
            go func(id int) {
                defer wg.Done()
                for j := 0; j < 5; j++ {
                    ch <- j + id*10
                }
            }(i)
        }
        
        // 消费者
        go func() {
            wg.Wait()
            close(ch)
        }()
        
        for value := range ch {
            fmt.Println("Received:", value)
        }
        
        1
        2
        3
        4
        5
        6
        7
        8
        9
        10
        11
        12
        13
        14
        15
        16
        17
        18
        19
        20
        21
        22
        23

    # 11. Channel 的死锁问题

    • 问题: 什么情况下使用 Channel 会导致死锁?如何避免?
    • 回答要点:
      • 典型的死锁情况:
        • 没有接收者的无缓冲 Channel 上进行发送。
        • 没有发送者的无缓冲 Channel 上进行接收。
        • 所有 case 都被阻塞的 select 语句且没有 default case。
      • 避免死锁的方法:
        • 确保每个 Channel 操作都有对应的发送或接收操作。
        • 使用带缓冲的 Channel 时注意容量的合理设置。
        • 在 select 语句中提供 default case 以避免完全阻塞。

    # 12. Channel 与 WaitGroup 的比较

    • 问题: Channel 和 sync.WaitGroup 都可以用于同步 Goroutine,二者有什么区别和适用场景?
    • 回答要点:
      • Channel:
        • 适用于 Goroutine 之间的数据传递和通信。
        • 可以使用 select 语句进行多路复用和超时控制。
      • WaitGroup:
        • 主要用于等待一组 Goroutine 执行完成。
        • 不涉及数据传递,仅用于同步多个 Goroutine 的完成信号。

    # 13. Channel 的关闭与多接收者

    • 问题: 如果一个 Channel 被关闭了,多个接收者 Goroutine 会如何处理?会出现什么问题?
    • 回答要点:
      • 当一个 Channel 被关闭后,多个接收者 Goroutine 会正常接收剩余数据,当数据接收完后,接收到的值为该类型的零值,并且 ok 为 false。
      • 没有数据的情况下,接收者 Goroutine 会立即完成接收并退出。
      • 在关闭 Channel 之前需要确保所有的发送操作

    # 14. channel 的底层实现原理

    # 基本结构

    channel 在 Go 中是一个引用类型,它的底层实现依赖于一个结构体 hchan,该结构体定义在 runtime 包中。
    hchan 结构体的主要字段包括:
      ```golang
        qcount:当前通道中的元素个数。
        dataqsiz:环形队列的大小,即通道缓冲区的大小。
        buf:实际存放数据的缓冲区,是一个数组指针。
        elemsize:每个元素的大小,用于内存计算。
        closed:表示通道是否已关闭。
        lock: 不允许并发操作, 通过mutex保证并发安全,循环队列的读写都要先加锁
        sendx 和 recvx:指示发送和接收操作的当前索引,用于在环形队列中进行操作。
        sendq 和 recvq:分别是阻塞发送和接收 goroutine 的队列,用于处理没有缓冲或者缓冲区满/空的情况。
      ```
    

    无缓冲和有缓冲区别: 管道没有缓冲区,从管道读数据会阻塞,直到有协程向管道中写入数据。同样,向管道写入数据也会阻塞,直到有协程从管道读取数据。管道有缓冲区但缓冲区没有数据,从管道读取数据也会阻塞,直到协程写入数据,如果管道满了,写数据也会阻塞,直到协程从缓冲区读取数据。

    # channel 的一些特点

    - 读写值 nil 管道会永久阻塞  
    - 关闭的管道读数据仍然可以读数据  
    - 往关闭的管道写数据会 panic  
    - 关闭为 nil 的管道 panic  
    - 关闭已经关闭的管道 panic  
    

    # 向 channel 写数据的流程:

    如果等待接收队列 recvq 不为空,说明缓冲区中没有数据或者没有缓冲区,此时直接从 recvq 取出 G,并把数据写入,最后把该 G 唤醒,结束发送过程; 如果缓冲区中有空余位置,将数据写入缓冲区,结束发送过程; 如果缓冲区中没有空余位置,将待发送数据写入 G,将当前 G 加入 sendq,进入睡眠,等待被读 goroutine 唤醒;

    # 向 channel 读数据的流程:

    如果等待发送队列 sendq 不为空,且没有缓冲区,直接从 sendq 中取出 G,把 G 中数据读出,最后把 G 唤醒,结束读取过程; 如果等待发送队列 sendq 不为空,此时说明缓冲区已满,从缓冲区中首部读出数据,把 G 中数据写入缓冲区尾部,把 G 唤醒,结束读取过程; 如果缓冲区中有数据,则从缓冲区取出数据,结束读取过程;将当前 goroutine 加入 recvq,进入睡眠,等待被写 goroutine 唤醒;

    # 使用场景:

    消息传递、消息过滤,信号广播,事件订阅与广播,请求、响应转发,任务分发,结果汇总,并发控制,限流,同步与异步

    编辑 (opens new window)
    上次更新: 2024/09/13, 11:59:12
    context
    go map

    ← context go map→

    最近更新
    01
    ceph分布式存储-对象存储(RGW)搭建
    10-27
    02
    ceph分布式存储-集群客户端连接
    10-27
    03
    ceph分布式存储-管理crushmap
    10-27
    更多文章>
    Theme by Vdoing
    • 跟随系统
    • 浅色模式
    • 深色模式
    • 阅读模式