Channels In Go

OpsDev - 高广鹏 360云计算

女主宣言

Go有两个重要的内置功能,同时也是它的特性。分别是channel、Goroutine。这两个特性使Go编写并发程序变的简单、有趣。本文将主要介绍channel。原文来自go101,本文是翻译后留存,方便自己学习。 PS:丰富的一线技术、多元化的表现形式,尽在“HULK一线技术杂谈”,点关注哦!

Channel Introduction



不要通过共享内存来通信,应该通过通信来共享内存,出自Rob Pike。是在Go社区非常流行的一句话,而channel就是为此而生。

通过共享内存来通信与通过同通信来共享内存是两种模式的并发程序。当通过共享内存来通信,我们需要使用一些传统的并发技术,例如互斥锁,来保证共享内存可以被安全的访问,防止数据竞争。

channel是在多个Goroutine之间传递数据和同步的重要手段,而对通道的操作本身也是同步的。同一时刻,仅有一个Goroutine能向一个channel发送元素值,同时仅有一个Goroutine能从它那里接受元素值。在channel中,各个元素值都是严格按照发送到此的先后顺序排序,最早被发送至channel的元素值会最先被接收。它类似一个内部的FIFO(first in,first out先进先出)数据队列。

此外,channel中的元素值都具有原子性,是不可被分割的。channel中的每个元素都只能被某一个Goroutine接收,已被接收的元素值会立刻从channel中删除。

某些value的使用权会随着value在Goroutines间传递,当Goroutine发送数据到channel会释放value的所有权,而在接收数据时会同时获得value的所有权。

Go也支持一些传统的并发技术,但channel应该优先被考虑。

老实说,每个并发同步技术都有其最佳的使用场景。 但channel应用范围更广,使用场景更多。 而且在许多情况下,使用channel的并发代码通常比使用其他数据同步处理技术看起来更清晰和易于理解。

1

Channel 的分类

channel是复合类型,类似array、slice、map,每个channel都有一个元素类型。 所有要发送到通道的数据都必须是元素类型的值。channel可以为nil

channel可分为双向、单向,假设以下的T是任意类型:

  • chan T, 双向channel,同时允许发送数据到channel、从channel接收数据。
  • chan<- T,单向channel,只允许发送数据到channel,操作符<-形象的表示了元素值的流向。
  • <-chan T,单向channel,只允许从channel接收数据,这次操作符<-位于关键字chan左边,这真的很棒。 使用内置的make函数可以创建一个channel,下面这个例子会创建一个元素类型为int的channel,make函数的第二个参数为可选参数,可以设置channel的容量,默认为0。

2

Channel 的操作

这里有5个channel的操作,假设ch为channel类型

  1. 关闭channel close是一个内置函数,参数必须是channel类型变量且不能是<-ch类型。

    1. 发送值到channel

    2. 从channel接收值

接收操作至少返回一个元素类型的值,它也可以用作赋值表达式

4. 查看channel容量

cap是一个内置函数,返回的值为int类型

5. 查看channel中当前值的数量

len是一个内置函数,返回的值为int类型。 如果channel元素类型为nil,cap、len将返回0。

3

Channel 操作规则总结

为了能将channel解释清楚,剩余的文章中会将channel分为三类:

  1. nil channel
  2. non-nil但已关闭的channel
  3. not-nil未关闭的channel

下面这个表简单表述以上三类channel的操作场景

背景简介

表中5种未标记的场景,应用规则非常清晰:

  • 关闭nil channel或已关闭的channel会引发panic

  • 发送元素值至已关闭的channel会引发panic

  • 发送元素值至nil channel或从nil channel接收元素值,都会导致当前goroutine永久阻塞

未标记的4种场景在下面会详细解释 为了更好的理解channel,先了解下channel的内部结构。我们可以认为每个channel在内部维护3个队列

  1. receiving goroutine queue(简称RGQ)是没有大小限制的链表,队列中是阻塞的receiving goroutine,准备存储值的地址也与每个goroutine一起存储在队列中

  2. sending goroutine queue(简称SGQ)同样是没有大小限制的链表,队列中是阻塞的sending goroutine, 准备发送的值的地址也与每个goroutine一起存储在队列中

  3. value buffer queue(简称VBQ)是个圆形队列,大小等于channel的容量。如果队列中值的数量达到channel的容量,channel会以full状态被调用。如果队列中没有存储值,channel会以emply状态被调用。容量为0的channel只能是full或emply。

Channel 规则场景 A

当gorontine尝试从非nil未关闭channel接收元素值,该goroutine先尝试获得channel关联的锁,然后执行以下步骤,直到满足一个条件。

  1. 如果channel的VBQ不为空,这种情况下channel的RGQ必须为空,此时的goroutine将通过unshift从VBQ接收元素值。 如果channel的SGQ也不为空,则将SGQ中的某个sending goroutine通过unshift移出队列并设置为运行状态,要发送的元素值将被放入channel的VBQ,receiving goroutine继续运行。该场景下channel的发送操作是非阻塞的

  2. 如果channel的VBQ为空,但SGQ不为空。这种情况下channel必须为非缓冲channel。receiving goroutine将从RGQ中unshift出某个sending goroutine,并接收这个sending goroutine发送的元素值。该sending goroutine将被解锁并设置为运行状态。该场景下channel的发送操作是非阻塞的

  3. 如果channel的VBQ和SGQ都为空,此时的gorontine会被放入RGQ中,进入(停留)在阻塞状态。当有其他goroutine发送元素值到channel,它可能恢复运行。该场景下channel的发送操作是阻塞的

Channel 规则场景 B

当goroutine尝试发送元素值至非空未关闭的channel,该goroutine先尝试获取channel关联的锁,然后执行以下步骤,直到满足一个条件。

  1. 如果channel的RGQ不为空,这种情况下VBG必须为空,sending goroutine将从RGQunshift出某个receiving goroutine,并发送元素值至这个receiving goroutine。sending goroutine继续运行。该场景下channel的发送是非阻塞的。

  2. 如果channel的RGQ为空,并且VBQ没有满。在这种情况下SGQ必须为空。将sending goroutine要发送的元素值放入VBQ,sending goroutine继续运行。该场景下channel的发送操作是非阻塞的。

  3. 如果channel的RGQ为空,并且VBQ已满。sending goroutine将被放入SGQ,进入(停留)在阻塞状态。当有其他goroutine从channel接收元素值,它可能恢复运行。该场景下channel的发送操作是阻塞的。

Channel 规则场景 C

当goroutine尝试关闭一个非空未关闭的channel,将按照以下顺序执行两个步骤

  1. 如果channel的RGQ不为空,在这种情况下VBQ必须为空。RGQ中所有goroutine会被逐个unshift,并且每个goroutine会接收到一个元素值类型的零值。

  2. 如果channel的SGQ不为空,SGQ中所有gorontine会被逐个unshift,每个向已关闭channel发送元素值的goroutine都会产生一个panic。已经放入到VBG的元素值仍然存在。

Channel 规则场景 D

channel关闭后,channel的接收操作将不会再被阻塞。VBQ已有的元素值可以继续被接收。当VBQ中所有的元素值都被取出后,后续的接收操作都会收到元素值的零值。

通过上述的规则,我们可以得到一些事实

  • 如果channel是关闭的,它的VGQ、SGQ必须为空,但VBQ可以不为空
  • 任何情况下,如果VBQ不为空,那么它的RGQ必须为空
  • 任何情况下,如果VBQ没满,那么它的SGQ必须为空
  • 缓冲channel在任何情况下,SBQ、RGQ其中之一必须为空
  • 非缓冲channel在任何情况下,通常SBQ、RGQ其中之一必须为空,但有一个例外,那就是select可能会导致某个goroutine被放入到这两个队列中。

4

Channel 使用实例

现在来看些channel使用的例子


package main
import "fmt"
func main() {   
    c := make(chan int) // 非缓冲通道    
    go func() {        
        x := <- c // 这里会被阻塞,直到通道收到元素值        
        c <- x*x  // 这里会被阻塞,直到通道中的值被接收    
    }()
    c <- 3   // 这里会被阻塞,直到通道中的值被接收     
    y := <-c // 这里会被阻塞,直到通道收到元素值        fmt.Println(y) // 9
}

下面这个例子使用了缓冲通道,但这个程序不是并发的


package main
import "fmt"
func main() {
    c := make(chan int, 2) // 缓冲通道
    c <- 3
    c <- 5
    close(c)
    fmt.Println(len(c), cap(c)) // 2 2
    x, ok := <-c
    fmt.Println(x, ok) // 3 true
    fmt.Println(len(c), cap(c)) // 1 2
    x, ok = <-c
    fmt.Println(x, ok) // 5 true
    fmt.Println(len(c), cap(c)) // 0 2
    x, ok = <-c
    fmt.Println(x, ok) // 0 false
    x, ok = <-c
    fmt.Println(x, ok) // 0 false
    fmt.Println(len(c), cap(c)) // 0 2
    close(c) // panic!
    c <- 7   // also panic if the last line is removed.
}

一场永不停止的足球赛


package main
import (
    "fmt"
    "time"
)
func main() {
    var ball = make(chan string)
    kickBall := func(playerName string) {
        for {
            fmt.Println(<-ball, "kicked the ball.")                        time.Sleep(time.Second)
            ball <- playerName
           }
    }
   go kickBall("John")
   go kickBall("Alice") 
   go kickBall("Bob")  
   go kickBall("Emily")  
   ball <- "referee" // kick off  
   var c chan bool   // nil 
   <-c               // blocking here for ever
}

5

Channel 元素值通过拷贝传递

无论发送元素值至channel还是从channel接收元素值,都会对这个值进行拷贝。类似赋值、函数传参操作。

标准的go编译器,要求channel元素类型不能超过65535。通常,我们不会限制channel的大小,所有通过channel传送的元素值都会发生拷贝。当某个元素值从一个goroutine被传递到另外一个goroutine,会有2个元素值被拷贝。所以如果传送的值很大,最好还是用指针来代替。

6

Channel 中的 For-Range 循环

for-range代码结构也可以应用到channel。循环将尝试迭代的接收channel中的,直到channel被关闭并且VBQ为空。


for v = range aChannel { 
   // use v
}

等同于


for {
    v, ok = <-aChannel
    if !ok { 
       break
    }
    // use v
}

这里的aChannel不能是只允许发送的通道类型。如果它是一个nil channel, 循环将永久阻塞。