channel¶
channel 的底层实现?¶
-
数据结构: Go 语言中的 channel 是用结构体 hchan 表示的,底层结构主要包括以下几个部分:
- 一个用于存储数据的环形缓冲区(对于带缓冲的 channels)。
- 一个互斥锁,用于保护对 channel 的并发访问。
- 两个等待队列:一个用于等待数据的发送 goroutines,另一个用于等待数据的接收 goroutines。
- 一些状态信息,如缓冲区大小、当前存储的元素数量、下一次发送(接收)数据的下标位置等。
-
发送数据:
当一个 goroutine 尝试发送数据到一个 channel 时,以下几种情况可能发生:
- 如果有一个或多个正在等待的接收 goroutine,发送操作将直接把数据传递给一个接收者并立即返回。
- 如果 channel 有空闲的缓冲区,数据将被存放到缓冲区并且发送操作立即返回。
- 否则,发送 goroutine 会被置于等待队列中,并被阻塞,直到数据可以被接收或存放到缓冲区为止。
-
接收数据:
当一个 goroutine 尝试从一个 channel 接收数据时:
- 如果有一个或多个正在等待的发送 goroutine,接收操作将直接从发送者那里获取数据并立即返回。
- 如果 channel 的缓冲区中有数据,数据会被移除并返回给接收者。
- 否则,接收 goroutine 会被置于等待队列中,并被阻塞,直到有数据可接收为止。
-
关闭 channel:
当一个 channel 被关闭时,所有等待的接收 goroutines 会被唤醒并接收到一个零值和一个 false 的第二返回值,表示 channel 已关闭并且没有更多数据。
-
同步与并发:
channel 的底层实现确保了在任何时候只有一个 goroutine 可以执行发送或接收操作。这是通过内部的互斥锁和等待队列来实现的。
总的来说,channel 的底层实现是围绕环形缓冲区、等待队列、互斥锁以及相关状态进行的。其设计使得 goroutines 可以安全、有效地同步并通信。为了完全理解其底层工作原理,建议直接查看 Go 的源代码,特别是 runtime 包中与 channel 相关的部分。
channel 有缓冲区和无缓冲区的区别?¶
-
创建方式:
- 无缓冲区的 channel:
ch := make(chan int) - 有缓冲区的 channel:
ch := make(chan int, n)其中n是缓冲区大小。
- 无缓冲区的 channel:
-
行为:
- 无缓冲区的 channel:发送和接收操作都是同步的。当一个值被发送到一个无缓冲的 channel 上时,发送操作会阻塞,直到另一个 Go 协程从这个 channel 上接收这个值。类似地,当一个 Go 协程试图从一个无缓冲的 channel 接收值时,如果 channel 为空,那么接收操作会阻塞,直到另一个 Go 协程发送一个值到这个 channel。
- 有缓冲区的 channel:发送和接收操作可能是异步的,只要缓冲区不满或不为空。当缓冲区满时,发送操作会阻塞,直到有空间可用;当缓冲区为空时,接收操作会阻塞,直到有值可用。
-
应用场景:
- 无缓冲区的 channel:经常被用作同步点,例如等待一个任务完成或等待数据准备好。无缓冲的 channel 是确保两个协程同步执行的一种方式。
- 有缓冲区的 channel:当你希望解耦生产数据的速度和消费数据的速度时,有缓冲的 channel 是有用的。它们还可以用于实现工作队列,其中发送操作不需要立即由接收操作来匹配。
-
注意事项:
- 尽管有缓冲区的 channel 可以提高某些情况下的性能,但如果缓冲区太大,它可能会隐藏程序中的竞争条件或死锁,使问题更难以调试。
- 使用无缓冲的 channel 可以更容易地确保程序的确定性,因为发送和接收操作是同步的。
channel 什么时候会发生阻塞¶
在 Go 中,channel 的操作(如发送数据和接收数据)在某些情况下会导致 goroutine 阻塞。以下是引起阻塞的常见情况:
-
发送数据到已满的缓冲 channel:
- 对于带缓冲的 channel,如果缓冲区已满,尝试发送数据会导致发送 goroutine 阻塞,直到有其他 goroutine 从 channel 中接收数据并为新数据腾出空间。
-
从空的缓冲 channel 接收数据:
- 如果缓冲区为空,尝试从 channel 接收数据会导致接收 goroutine 阻塞,直到有其他 goroutine 向 channel 发送数据。
-
发送数据到没有接收者的非缓冲 channel:
- 对于不带缓冲的 channel(或称为同步 channel),发送操作要求有一个接收操作与之匹配。如果没有可用的接收者,发送 goroutine 会阻塞。
-
从没有发送者的非缓冲 channel 接收数据:
- 同样,对于非缓冲的 channel,接收操作要求有一个发送操作与之匹配。如果没有数据被发送到 channel,接收 goroutine 会阻塞。
-
尝试从已关闭且为空的 channel 接收数据:
- 如果一个 channel 已经关闭并且所有数据都已被接收,那么进一步的接收操作不会阻塞。相反,它会立即返回相应类型的零值。但这种情况并不会导致阻塞。
-
使用 select 语句时没有可用的 case:
- 当使用
select语句进行多路选择时,如果没有一个 case 可以执行(即所有 channel 操作都会阻塞),并且没有提供defaultcase,那么整个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 会发生什么?¶
-
未初始化的channel: 如果你尝试从一个未初始化的channel(nil channel)中读取或写入数据,你的goroutine将会阻塞,但这种阻塞是永久的,因为没有其他的goroutine会来初始化或关闭这个channel。在一个主goroutine中这样做会导致死锁。
-
已初始化但没有数据的channel: 如果你从一个已经初始化但是没有数据的channel读取数据,goroutine会阻塞,直到有其他的goroutine向该channel写入数据,或者该channel被关闭。
-
关闭的channel: 如果channel已经被关闭,那么从这个channel读取数据不会阻塞,它会立即返回该类型的零值。同时,你还可以通过第二个返回值来检测channel是否已关闭。
其中,
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 |