通道分为缓冲通道和无缓冲通道两种,在使用make内置函数创建通道大小的时候,会出现两个常见的错误:1. 是选择缓冲通道还是无缓冲通道?2. 如果是使用缓冲通道,通道的大小应该设置为多少?本节内容将深入研究这些问题。
首先记住一点,无缓冲通道是没有任何容量的通道。创建无缓冲通道时可以设置通道大小为0,或者不设置大小参数。代码如下:
ch1 := make(chan int)
ch2 := make(chan int, 0)
使用无缓冲通道,无缓冲通道有时也称为同步通道,在接收方从通道中接收数据之前,发送方将会被阻塞。相反,有缓冲通道具有一定容量,在创建的时候必须指定大小,并且大小大于0.
ch3 := make(chan int, 1)
使用缓冲通道,发送者可以在通道没有满的时候,一直往里面发送消息。一旦通道已满,发送操作会被阻塞,直到接收方goroutine收到消息。例如:
ch3 := make(chan int, 1)
ch3 <-1
ch3 <-2
在上面的程序中,第一次向通道ch3中发送数据1不会被阻塞,然而第二次向里面发送数据2时,将会被阻塞,因为此时通道满了。
现在开始讨论两种通道本质区别。通道是实现goroutine之间通信的并发抽象。什么是同步操作呢?在并发程序中,同步意味着我们可以保证多个goroutine在某个时刻处于已知状态。例如,互斥锁提供同步,因为它确保只有一个goroutine可以同时处于临界区。对于通道来说:
必须牢记它们之间基本区别,两种通道类型都支持通信。但只有一种提供同步。如果我们需要同步操作,必须使用无缓冲通道。此外无缓冲通道问题可能更容易排查,而缓冲通道会导致模糊的死锁问题,无缓冲通道存在问题会立即表现出来。在某些情况下,使用无缓冲通道更好。例如在通知通道的情况下,通知是通过通道关闭(close(ch))处理的,在这种情况下,使用缓冲通道不会带来任何好处。
回到文章开头提出的第二问题,如果要创建一个缓冲通道,通道的大小设置多少合适呢?有缓冲通道大小可以设置的最小值为1,这个是毫无疑问的。从这个角度来看,有什么更好的理由可以不使用1这个值吗? 下面是应该使用其他值的情况:
如果实际不是上面这些情况,在设置通道大小的时候则需要谨慎。事实上,经常看到代码库中使用一些神奇的数字来设置通道大小,例如:
ch := make(chan int, 40)
为什么设置通道的大小为40?理由是什么,为什么不设置为50?甚至100?设置这样的值应该要有充分的理由。也许,设置这个值是根据基准测试或性能测试之后决定的,在很多情况下,通过测试对比来设置是一个好的方法。需要注意的是,准确设置通道大小并不是一个容易的事情。涉及到CPU和内存之间的平衡,设置的越小,面对的CPU争用会越大,设置的越大,需要分配的内存会越多。
需要考虑的另一个点来自LMAX Disruptor文章(https://lmax-exchange.github.io/disruptor/files/Disruptor-1.0.pdf)。
❝由于消费者和生产者之间的速度差异,队列通常总是接近满或者接近空,很少能够在生产和消费均衡匹配的平衡中间地带运作。 ❞
所以很难找到一个既不会导致过多争用又不会浪费内存的稳定准确的通道大小。这就是为什么除了上面描述的情况之外,通常最好从默认值1开始设置通道大小。在不确定的情况下,可以通过实际测试来进行衡量评估。
总结,本节内容不能给出通道大小应该设置多少的准确量化,这几乎是不可能的。在不同的实际环境中值可能是不同的,所以很难用一个公式进行计算量化。同步是无缓冲通道带来的保证,有缓冲通道是没有任何同步保证的。如果要设置一个缓冲通道,应该知道其默认大小为1,如果要设置其他值需要谨慎,并且能够评估为什么设置成这个值合理。最后留意一点,使用缓冲通道也可能导致潜在的死锁问题,使用无缓冲通道当出现死锁的时候更容易发现。