使用 make
内置函数创建通道时,通道可以是无缓冲的或有缓冲的。在这个话题下,经常会发现两个常见错误:应该使用哪种类型的通道感到困惑,如果我们使用缓冲通道,我们应该使用什么大小?让我们深入研究这些要点。
首先,让我们记住核心概念。无缓冲通道是没有任何容量的通道。它可以通过省略大小或提供零大小来创建:
ch1 := make(chan int)
ch2 := make(chan int, 0)
使用无缓冲通道(有时也称为同步通道),发送方将阻塞,直到接收方从通道接收到数据。
相反,缓冲通道具有容量,并且必须以大于或等于 1 的大小创建它:
ch3 := make(chan int, 1)
使用缓冲通道,发送方可以在通道未满时发送消息。一旦通道已满,它将阻塞,直到接收者 goroutine 收到消息。例如:
ch3 := make(chan int, 1)
ch3 <-1
ch3 <-2
第一次发送没有阻塞,而第二次发送是在这个阶段通道已满。
让我们退后一步,讨论这两种通道类型之间的根本区别。
通道是实现 goroutine 之间通信的并发抽象。然而,同步呢?在并发中,同步意味着我们可以保证多个 goroutine 在某个时刻处于已知状态。例如,互斥锁提供同步,因为它确保只有一个 goroutine 可以同时处于临界区。关于通道:
- 无缓冲通道可实现同步。事实上,我们可以保证两个 goroutine 将处于已知状态:一个接收消息,另一个发送消息。
- 然而,缓冲通道不提供任何强同步。事实上,生产者 goroutine 可以发送一条消息,然后在通道未满时继续执行。唯一的保证是 goroutine 在发送之前不会收到消息。然而,这只是因果关系的保证(您在准备咖啡之前不要喝咖啡)。
必须牢记这一基本区别。两种通道类型都支持通信,但只有一种提供同步。如果我们需要同步,我们必须使用无缓冲通道。此外,无缓冲通道可能更容易调查 bug 原因。事实上,缓冲通道可能会导致模糊的死锁,这在无缓冲通道中会立即显现出来。
在其他情况下,无缓冲通道更可取。例如,在通知通道的情况下,通知是通过通道关闭(close(ch)
)。在这里,使用缓冲通道不会带来任何好处。
但是如果我们需要一个缓冲通道呢?我们将选择什么尺寸?
我们应该为缓冲通道使用的默认值是这个最小值:1。所以我们可以从这个角度来处理这个问题:有什么好的理由不使用一这个值吗?以下是我们应该使用其他尺寸的可能情况列表:
-
在使用类似工作池的模式时,这意味着旋转固定数量的 goroutine,需要将数据发送到共享通道。在这种情况下,我们可以将通道大小与数字联系起来创建的 goroutines。
-
使用通道解决速率限制问题时。例如,如果我们需要通过限制请求数量来强制资源利用率,我们应该根据限制设置通道大小。
如果我们不在这些情况下,则应谨慎使用不同的通道大小。确实,经常看到代码库使用一些幻数来设置通道大小:
ch := make(chan int, 40)
为什么是40?它的理由是什么?为什么不是50?甚至1000?设置这样的值应该是有充分理由的。也许,这是在基准测试或性能测试之后决定的。在许多情况下,评论这种值的基本原理可能是一个好主意。
让我们记住,确定准确的队列大小并不是一个容易的问题。首先,它是 CPU 和内存之间的平衡。值越小,我们可以面对的 CPU 争用就越多。然而,值越大,需要分配的内存就越多。
另一点需要考虑的是 LMAX Disruptor 白皮书中提到的
由于消费者和生产者之间的速度差异,队列通常总是接近满或接近空。它们很少在生产率和消费率均匀匹配的平衡中间地带运行。
-- LMAX Disruptor
所以,很难找到一个稳定准确的通道大小,这意味着一个准确的值不会导致太多的争用,也不会浪费内存分配。
这就是为什么除了所描述的情况之外,通常最好从默认通道大小 1 开始。例如,当不确定时,我们仍然可以使用基准来衡量它。
与编程中的几乎所有主题一样,都可以找到例外情况。因此,本节的目标不是详尽无遗,而是指导我们在创建通道时应该使用什么尺寸。同步是非缓冲通道带来的保证,而不是缓冲通道。此外,如果我们需要一个缓冲通道,我们应该记住通道大小的默认值应该是 1。应使用准确的过程谨慎决定是否具有另一个值,并且可能应该对基本原理进行评论。最后但同样重要的是,让我们记住,选择缓冲通道也可能导致潜在的模糊死锁,如果使用无缓冲通道更容易发现这种情况。
在下一节中,我们将讨论处理字符串格式时可能产生的副作用。