go-通道

通道可以让一个goroutine发送特定值到另一个goroutine。每一个通道是一个具体类型的导管,叫作通道的元素类型。一个有int类型元素的通道写作chan int。

通道

声明一个传递int类型的channel:

1
var ch chan int

使用内置的make函数来创建一个通道:

1
ch := make(chan int) // ch 的类型是 'chan int'

像map一样,通道是一个使用make创建的数据结构的引用。当赋值或者作为参数传递到一个函数时,赋值的是引用,这样调用者和被调用者都引用同一份数据结构。和其他引用类型一样,通道的零值是nil。
同种类型的通道可以使用==符号进行比较。当二者都是同一通道数据的引用时,比较值为true。通道也可以和nil进行比较。
通道有两个主要操作:发送(send) 和 接收(receive),两者统称为通信。send语句从一个goroutine传输一个值到另一个在执行接收表达式的goroutine。两个操作都是用<- 操作符书写。发送语句中,
通道和值分别在<-的左右两边。在接收表达式中,<-在通道操作数前面。在接收表达式中,其结果未被使用也是合法的:

1
2
3
ch <- x // 发送语句, 会导致阻塞,直到有其他goroutine从这个channel中读取数据
x = <-ch // 赋值语句中的接收表达式, 如果channel之前没有写入数据,也会导致阻塞,知道channel中被写入数据为止
<-ch // 接收语句,丢弃结果

通道支持第三个操作:关闭(close),它设置一个标志位来指示值当前已经发送完毕,这个通道后面没有值了;关闭后的发送操作将导致异常。在一个已经关闭的通道上进行接受操作,
将获取所有已经发送的值,直到通道为空;这是任何接收操作会立即完成,同时获取到一个通道元素类型对应的零值。
调用内置的close函数来关闭通道:

1
close(ch)

无缓冲通道

使用简单的make会调用创建的通道叫做无缓冲(unbuffered)通道,但make还可以接收第二个可选参数,一个表示通道容量的整数。如果容量是0,make创建一个无缓冲通道:

1
2
3
ch = make(chan int) // 无缓冲通道
ch = make(chan int, 0) // 无缓冲通道
ch = make(chan int, 3) // 容量为3的缓冲通道

无缓冲通道上的发送操作将会阻塞,直到另一个goroutine在对应的通道上执行接收操作,这时值传送完成,两个goroutine都可以继续执行。相反,如果接收操作先执行,接收方goroutine将阻塞,知道另一个goroutine在同一个通道上发送一个值。
使用无缓冲通道进行的通信导致发送和接收goroutine同步化。因为,无缓冲通道也称为同步通道。当一个值在无缓冲通道上传递时,接收值后发送方goroutine才被再次唤醒。

管道

通道可以用来连接goroutine,这样一个的输出是另一个的输入,这个叫管道(pipeline):

第一个goroutine是counter,产生一个0,1,2,…的整数序列,然后通过一个管道发送给第二个goroutine(叫square),计算数值的平方,然后将结果通过另一个通道发送给第三个goroutine(叫printer),接收值并输出他们。eg:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
func main() {
naturals := make(chan int)
squares := make(chan int)
// counter
go func() {
for x := 0; x < 100; x++ {
naturals <- x
}
close(naturals)
}()
// squarer
go func() {
for {
x := <-naturals
squares <- x * x
}
close(squares)
}()
// printer (在主goroutine中)
for x := range squares{
fmt.Println(x)
}
}

结束时,关闭每一个通道不是必须的。只有在通知接收方goroutine所有的数据都发送完毕的时候才需要关闭通道。通道也是可以通过垃圾回收器根据它是否可用访问来决定是否回收它,而不是根据它是否关闭。

单向通道类型

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
func counter(out chan <- int) {
for x := 0; x < 100; x++ {
out <- x
}
close(out)
}
func squarer(out chan <- int, in <- chan int) {
for v := range in {
out <- v * v
}
close(out)
}
func printer(in <- chan int) {
for v := range in {
fmt.Println(v)
}
}
func main() {
naturals := make(chan int)
squares := make(chan int)
go counter(naturals) //隐式的将chan int 类型转化为参数要求的 chan <- int类型
go squares(squares, naturals)
printer(squares) //做了类似 <- chan int 的转变
}

在任何赋值操作中将双向通道转换为单向通道都是允许的,但是反过来是不行的。

缓冲通道

缓冲通道有一个元素队列,队列的最大长度在创建的时候通过make的容量参数来设置:

1
ch = make(chan string, 3) // 创建一个可以容纳三个字符串的缓冲通道

缓冲通道上的发送操作在队列的尾部插入一个元素,接收操作从队列的头部移除一个元素。如果通道满了,发送操作会阻塞所在的goroutine直到另一个goroutine对他进行接收操作来留出可用的空间。反过来,如果通道是空的,执行接收操作的goroutine阻塞,直到另一个goroutine在通道上发送数据。

程序想要直到通道缓冲区的容量,可用通过调用函数cap:

1
fmt.Println(cap(ch)) // 3

当使用内置函数len时,可以获取当前通道内的元素个数。因为在并发程序中这个信息会随着检索操作很快过时,所以它的价值很低,但是它在错误诊断和性能优化的时候很有用。

select多路复用

在golang里头select的功能与epoll(nginx)/poll/select的功能类似,都是监听和channel有关的IO操作,当IO操作发生的时候,触发相应的动作。

基本语法

1
2
3
4
5
6
7
8
select {
case <= chan1:
// 如果chan1成功读到数据,则进行该case处理语句
case chan2 <- 1:
// 如果成功向chan2写入数据,则进行该case处理语句
default:
// 如果上面都没有成功,则进入default处理流程
}

注意:

  • 所有channel表达式都会被求值、所有被发送的表达式都会被求值,求值顺序:自上而下、从左到右。
  • 如果有多个case都可以运行,select会随机公平的选出一个执行,其他不会执行;否则的话,如果有default分支,则执行default分支语句;如果连default都没有,则select语句会一直阻塞到至少有一个IO操作可以进行

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    func main() {
    ch := make (chan int, 1)
    ch<-1
    select {
    case <-ch:
    fmt.Println("Layne") // 随机输出
    case <-ch:
    fmt.Println("Don") // 随机输出
    }
    }
  • case后面必须是channel操作,否则报错

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    func main() {
    ch := make (chan int, 1)
    ch<-1
    select {
    case <-ch:
    fmt.Println("Layne")
    case 2: // 该行报错
    fmt.Println("Don")
    }
    }

小结

  • 并发是指goroutine运行的时候是相互独立的
  • 使用关键字go创建goroutine来运行函数
  • goroutine在逻辑处理器上执行,而逻辑处理器具有独立的系统线程和运行队列
  • 竞争状态是指两个或者多个goroutine试图访问同一个资源
  • 原子函数和互斥锁提供了一种防止出现竞争状态的方法
  • 通道提供了一种在两个goroutine之间共享数据的简单方法
  • 无缓冲通道保证同时交换数据,而有缓冲的通道不做这种保证