Skip to content

channel

channel 的底层实现?

  1. 数据结构: Go 语言中的 channel 是用结构体 hchan 表示的,底层结构主要包括以下几个部分:

    • 一个用于存储数据的环形缓冲区(对于带缓冲的 channels)。
    • 一个互斥锁,用于保护对 channel 的并发访问。
    • 两个等待队列:一个用于等待数据的发送 goroutines,另一个用于等待数据的接收 goroutines。
    • 一些状态信息,如缓冲区大小、当前存储的元素数量、下一次发送(接收)数据的下标位置等。
  2. 发送数据:

    当一个 goroutine 尝试发送数据到一个 channel 时,以下几种情况可能发生:

    • 如果有一个或多个正在等待的接收 goroutine,发送操作将直接把数据传递给一个接收者并立即返回。
    • 如果 channel 有空闲的缓冲区,数据将被存放到缓冲区并且发送操作立即返回。
    • 否则,发送 goroutine 会被置于等待队列中,并被阻塞,直到数据可以被接收或存放到缓冲区为止。
  3. 接收数据:

    当一个 goroutine 尝试从一个 channel 接收数据时:

    • 如果有一个或多个正在等待的发送 goroutine,接收操作将直接从发送者那里获取数据并立即返回。
    • 如果 channel 的缓冲区中有数据,数据会被移除并返回给接收者。
    • 否则,接收 goroutine 会被置于等待队列中,并被阻塞,直到有数据可接收为止。
  4. 关闭 channel:

    当一个 channel 被关闭时,所有等待的接收 goroutines 会被唤醒并接收到一个零值和一个 false 的第二返回值,表示 channel 已关闭并且没有更多数据。

  5. 同步与并发:

    channel 的底层实现确保了在任何时候只有一个 goroutine 可以执行发送或接收操作。这是通过内部的互斥锁和等待队列来实现的。

总的来说,channel 的底层实现是围绕环形缓冲区、等待队列、互斥锁以及相关状态进行的。其设计使得 goroutines 可以安全、有效地同步并通信。为了完全理解其底层工作原理,建议直接查看 Go 的源代码,特别是 runtime 包中与 channel 相关的部分。

channel 有缓冲区和无缓冲区的区别?

  1. 创建方式:

    • 无缓冲区的 channel:ch := make(chan int)
    • 有缓冲区的 channel:ch := make(chan int, n) 其中 n 是缓冲区大小。
  2. 行为:

    • 无缓冲区的 channel:发送和接收操作都是同步的。当一个值被发送到一个无缓冲的 channel 上时,发送操作会阻塞,直到另一个 Go 协程从这个 channel 上接收这个值。类似地,当一个 Go 协程试图从一个无缓冲的 channel 接收值时,如果 channel 为空,那么接收操作会阻塞,直到另一个 Go 协程发送一个值到这个 channel。
    • 有缓冲区的 channel:发送和接收操作可能是异步的,只要缓冲区不满或不为空。当缓冲区满时,发送操作会阻塞,直到有空间可用;当缓冲区为空时,接收操作会阻塞,直到有值可用。
  3. 应用场景:

    • 无缓冲区的 channel:经常被用作同步点,例如等待一个任务完成或等待数据准备好。无缓冲的 channel 是确保两个协程同步执行的一种方式。
    • 有缓冲区的 channel:当你希望解耦生产数据的速度和消费数据的速度时,有缓冲的 channel 是有用的。它们还可以用于实现工作队列,其中发送操作不需要立即由接收操作来匹配。
  4. 注意事项:

    • 尽管有缓冲区的 channel 可以提高某些情况下的性能,但如果缓冲区太大,它可能会隐藏程序中的竞争条件或死锁,使问题更难以调试。
    • 使用无缓冲的 channel 可以更容易地确保程序的确定性,因为发送和接收操作是同步的。

channel 什么时候会发生阻塞

在 Go 中,channel 的操作(如发送数据和接收数据)在某些情况下会导致 goroutine 阻塞。以下是引起阻塞的常见情况:

  1. 发送数据到已满的缓冲 channel:

    • 对于带缓冲的 channel,如果缓冲区已满,尝试发送数据会导致发送 goroutine 阻塞,直到有其他 goroutine 从 channel 中接收数据并为新数据腾出空间。
  2. 从空的缓冲 channel 接收数据:

    • 如果缓冲区为空,尝试从 channel 接收数据会导致接收 goroutine 阻塞,直到有其他 goroutine 向 channel 发送数据。
  3. 发送数据到没有接收者的非缓冲 channel:

    • 对于不带缓冲的 channel(或称为同步 channel),发送操作要求有一个接收操作与之匹配。如果没有可用的接收者,发送 goroutine 会阻塞。
  4. 从没有发送者的非缓冲 channel 接收数据:

    • 同样,对于非缓冲的 channel,接收操作要求有一个发送操作与之匹配。如果没有数据被发送到 channel,接收 goroutine 会阻塞。
  5. 尝试从已关闭且为空的 channel 接收数据:

    • 如果一个 channel 已经关闭并且所有数据都已被接收,那么进一步的接收操作不会阻塞。相反,它会立即返回相应类型的零值。但这种情况并不会导致阻塞。
  6. 使用 select 语句时没有可用的 case:

    • 当使用 select 语句进行多路选择时,如果没有一个 case 可以执行(即所有 channel 操作都会阻塞),并且没有提供 default case,那么整个 select 会阻塞。

我能不能在写入 channel 的时候,先判断它阻不阻塞再写?

是的,你可以使用 Go 的 select 语句在尝试写入 channel 之前先检查它是否会阻塞。使用 select 的非阻塞模式,你可以先尝试一个操作,并在该操作无法立即完成时执行其他操作或回退。

以下是一个简单的示例,展示了如何检查 channel 是否可以无阻塞地写入:

ch := make(chan int, 2)
ch <- 1  // 填充channel,使其快速满

select {
case ch <- 2:
    fmt.Println("Wrote to channel successfully.")
default:
    fmt.Println("Channel was full, did not write.")
}

在上面的例子中,ch 是一个带有容量为 2 的缓冲 channel。首先,我们写入一个值使其部分填满。然后我们尝试再次写入。select 语句允许我们尝试写入,如果 channel 已满(即写入操作会阻塞),则立即回退到 default case。

需要注意的是,非阻塞的 select 应该谨慎使用,因为不当的使用可能会导致忙等待,从而消耗 CPU 资源。此外,根据具体的使用场景,有时最佳的做法是让 goroutine 阻塞,而不是频繁地检查 channel 是否可用。

读取空 channel 会发生什么?

  1. 未初始化的channel: 如果你尝试从一个未初始化的channel(nil channel)中读取或写入数据,你的goroutine将会阻塞,但这种阻塞是永久的,因为没有其他的goroutine会来初始化或关闭这个channel。在一个主goroutine中这样做会导致死锁。

    var ch chan int // ch 是 nil
    <-ch            // 会永久阻塞
    
  2. 已初始化但没有数据的channel: 如果你从一个已经初始化但是没有数据的channel读取数据,goroutine会阻塞,直到有其他的goroutine向该channel写入数据,或者该channel被关闭。

    ch := make(chan int)
    go func() {
        time.Sleep(2 * time.Second)
        ch <- 42
    }()
    fmt.Println(<-ch) // 这会阻塞,直到上面的goroutine发送数据
    
  3. 关闭的channel: 如果channel已经被关闭,那么从这个channel读取数据不会阻塞,它会立即返回该类型的零值。同时,你还可以通过第二个返回值来检测channel是否已关闭。

    ch := make(chan int)
    close(ch)
    val, ok := <-ch
    fmt.Println(val, ok) // 输出 "0 false"
    

    其中,val 是从channel读取的值(对于int类型channel,关闭的channel读取会返回0),而 ok 是一个布尔值,当channel已经关闭并且所有的值都已经被读取时,它会返回false。这可以用来检测channel是否已经关闭。

以下是一个表格,描述了在不同channel状态下进行读、写和关闭操作的结果:

状态\操作 读取 (<-ch) 写入 (ch <-) 关闭 (close(ch))
未初始化 (nil) 永久阻塞 永久阻塞 panic: close of nil chan
空但已初始化 阻塞,直到有数据或 channel 关闭 阻塞,直到有读取或 channel 关闭 可以关闭,后续读操作会立即返回零值
有数据 立即返回数据 可能阻塞,直到有足够空间或有读取 可以关闭,后续读操作会返回已有数据,然后返回零值
已关闭 如果还有数据,返回数据;无数据则立即返回零值 panic: send on closed channel panic: close of closed channel