Channels In Go

阅读数:48 2019 年 11 月 21 日 23:47

Channels In Go

Go 有两个重要的内置功能,同时也是它的特性。分别是 channel、Goroutine。这两个特性使 Go 编写并发程序变的简单、有趣。本文将主要介绍 channel。原文来自 go101,本文是翻译后留存,方便自己学习。

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。

Channels In Go

2 Channel 的操作

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

1. 关闭 channel

Channels In Go

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

  1. 发送值到 channel

Channels In Go

  1. 从 channel 接收值

Channels In Go

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

Channels In Go

  1. 查看 channel 容量

Channels In Go

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

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

Channels In Go

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

如果 channel 元素类型为 nil,cap、len 将返回 0。

3 Channel 操作规则总结

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

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

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

Channels In Go

背景简介

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

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

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

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

未标记的 4 种场景在下面会详细解释

1. 为了更好的理解 channel,先了解下 channel 的内部结构。我们可以认为每个 channel 在内部维护 3 个队列

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

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

4.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, 循环将永久阻塞。

本文转载自公众号 360 云计算(ID:hulktalk)。

原文链接:

https://mp.weixin.qq.com/s/xIeoClbb6wszqLRNkaV2yw

评论

发布